Crítica do protocolo e abordagens organizacionais do Telegram. Parte 1, técnica: experiência de escrever um cliente do zero - TL, MT

Recentemente, postagens sobre o quão bom o Telegram é, quão brilhantes e experientes os irmãos Durov são na construção de sistemas de rede, etc. começaram a aparecer com mais frequência no Habré. Ao mesmo tempo, muito poucas pessoas realmente mergulharam no dispositivo técnico - no máximo, elas usam uma API de bot bastante simples (e bem diferente do MTProto) baseada em JSON, e geralmente apenas aceitam na fé todos os elogios e relações públicas que giram em torno do mensageiro. Quase um ano e meio atrás, meu colega da ONG Eshelon, Vasily (infelizmente, sua conta no Habré foi apagada junto com o rascunho) começou a escrever seu próprio cliente Telegram do zero em Perl, e mais tarde o autor dessas linhas aderiu. Por que Perl, alguns perguntarão imediatamente? Porque esses projetos já existem em outras línguas. Na verdade, não é esse o ponto, poderia haver qualquer outra língua onde não exista biblioteca pronta, e portanto o autor deve percorrer todo o caminho a partir do zero. Além disso, a criptografia é uma questão de confiança, mas de verificação. Com um produto voltado para segurança, você não pode simplesmente contar com uma biblioteca pronta do fabricante e confiar cegamente nela (porém, esse é um assunto para a segunda parte). No momento, a biblioteca funciona muito bem no nível “médio” (permite fazer qualquer solicitação de API).

No entanto, não haverá muita criptografia ou matemática nesta série de postagens. Mas haverá muitos outros detalhes técnicos e muletas arquitetônicas (úteis também para quem não vai escrever do zero, mas vai usar a biblioteca em qualquer idioma). Então, o objetivo principal foi tentar implementar o cliente do zero de acordo com documentação oficial. Ou seja, vamos supor que o código-fonte dos clientes oficiais seja fechado (novamente, na segunda parte abordaremos com mais detalhes o tópico do fato de que isso é verdade então), mas, como antigamente, por exemplo, existe um padrão como o RFC - é possível escrever um cliente apenas de acordo com a especificação, “sem olhar” o código-fonte, seja ele oficial (Telegram Desktop, celular) ou Teleton não oficial?

Sumário:

Documentação... existe, certo? É verdade?..

Fragmentos de notas deste artigo começaram a ser coletados no verão passado. Todo esse tempo no site oficial https://core.telegram.org A documentação era da camada 23, ou seja, preso em algum lugar em 2014 (lembra, não havia canais naquela época?). Claro que, em teoria, isso deveria ter nos permitido implementar um cliente com funcionalidade naquela época de 2014. Mas mesmo neste estado, a documentação era, em primeiro lugar, incompleta e, em segundo lugar, em alguns pontos ela se contradizia. Há pouco mais de um mês, em setembro de 2019, era acidentalmente Foi descoberto que houve uma grande atualização da documentação do site, para o Layer 105 completamente recente, com a observação de que agora tudo precisa ser lido novamente. Na verdade, muitos artigos foram revisados, mas muitos permaneceram inalterados. Portanto, ao ler as críticas abaixo sobre a documentação, você deve ter em mente que algumas dessas coisas não são mais relevantes, mas outras ainda são bastante. Afinal, 5 anos no mundo moderno não é apenas muito tempo, mas muito um monte de. Desde aquela época (especialmente se você não levar em conta os sites de geochat descartados e revividos desde então), o número de métodos API no esquema cresceu de cem para mais de duzentos e cinquenta!

Por onde começar como um jovem autor?

Não importa se você escreve do zero ou usa, por exemplo, bibliotecas prontas como Teleton para Python ou Madeline para PHP, em qualquer caso, você precisará primeiro cadastre seu aplicativo - obter parâmetros api_id и api_hash (aqueles que trabalharam com a API VKontakte entendem imediatamente) pelo qual o servidor identificará o aplicativo. Esse ter faça isso por motivos legais, mas falaremos mais sobre por que os autores de bibliotecas não podem publicá-lo na segunda parte. Você pode ficar satisfeito com os valores dos testes, embora sejam muito limitados - o fato é que agora você pode se cadastrar apenas um aplicativo, então não se precipite.

Agora, do ponto de vista técnico, devemos nos interessar pelo fato de que após o cadastro deveremos receber notificações do Telegram sobre atualizações de documentação, protocolo, etc. Ou seja, pode-se supor que o local com as docas foi simplesmente abandonado e continuou a trabalhar especificamente com quem começou a fazer clientes, pois é mais fácil. Mas não, não foi observado nada disso, não chegou nenhuma informação.

E se você escrever do zero, ainda está muito longe de usar os parâmetros obtidos. Embora https://core.telegram.org/ e fala sobre eles em Primeiros passos, na verdade, primeiro você terá que implementar Protocolo MTProto - mas se você acreditasse layout de acordo com o modelo OSI no final da página para uma descrição geral do protocolo, então é completamente em vão.

Na verdade, antes e depois do MTProto, em vários níveis ao mesmo tempo (como dizem os profissionais de rede estrangeiros que trabalham no kernel do sistema operacional, violação de camada), um tópico grande, doloroso e terrível irá atrapalhar...

Serialização binária: TL (Type Language) e seu esquema, camadas e muitas outras palavras assustadoras

Este tópico, aliás, é a chave dos problemas do Telegram. E haverá muitas palavras terríveis se você tentar se aprofundar nisso.

Então, aqui está o diagrama. Se esta palavra vier à sua mente, diga: Esquema JSON, Você pensou corretamente. O objetivo é o mesmo: alguma linguagem para descrever um possível conjunto de dados transmitidos. É aqui que as semelhanças terminam. Se da página Protocolo MTProto, ou da árvore fonte do cliente oficial, tentaremos abrir algum esquema, veremos algo como:

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;

Uma pessoa que vê isso pela primeira vez será capaz de reconhecer intuitivamente apenas parte do que está escrito - bem, aparentemente são estruturas (embora onde está o nome, à esquerda ou à direita?), existem campos nelas, após o qual um tipo segue após dois pontos... provavelmente. Aqui entre colchetes angulares provavelmente existem modelos como em C++ (na verdade, não é bem assim). E o que significam todos os outros símbolos, pontos de interrogação, pontos de exclamação, porcentagens, hash (e obviamente significam coisas diferentes em lugares diferentes), às vezes presentes e às vezes não, números hexadecimais - e o mais importante, como sair disso direito (que não será rejeitado pelo servidor) fluxo de bytes? Você terá que ler a documentação (sim, existem links para o esquema na versão JSON nas proximidades - mas isso não deixa tudo mais claro).

Abra a página Serialização de dados binários e mergulhe no mundo mágico dos cogumelos e da matemática discreta, algo semelhante ao matan no 4º ano. Alfabeto, tipo, valor, combinador, combinador funcional, forma normal, tipo composto, tipo polimórfico... e isso é tudo apenas a primeira página! O próximo espera por você Idioma TL, que, embora já contenha um exemplo de solicitação e resposta triviais, não fornece nenhuma resposta para casos mais típicos, o que significa que você terá que percorrer uma recontagem de matemática traduzida do russo para o inglês em mais oito embutidos Páginas!

Os leitores familiarizados com linguagens funcionais e inferência automática de tipos verão, é claro, a linguagem de descrição nesta linguagem, mesmo a partir do exemplo, como muito mais familiar, e podem dizer que isso não é ruim em princípio. As objeções a isso são:

  • sim objetivo parece bom, mas, infelizmente, ela não alcançado
  • A educação nas universidades russas varia até mesmo entre as especialidades de TI - nem todos fizeram o curso correspondente
  • Finalmente, como veremos, na prática é Não exige, uma vez que apenas um subconjunto limitado do TL que foi descrito é usado

Como disse Leo Nerd no canal #perl na rede FreeNode IRC, tentando implementar uma porta do Telegram para o Matrix (a tradução da cotação é imprecisa da memória):

Parece que alguém foi apresentado à teoria dos tipos pela primeira vez, ficou animado e começou a tentar brincar com ela, sem se importar se isso era necessário na prática.

Veja você mesmo, se a necessidade de tipos simples (int, long, etc.) como algo elementar não levanta questões - em última análise, eles devem ser implementados manualmente - por exemplo, vamos tentar derivar deles vetor. Isto é, na verdade, matriz, se você chamar as coisas resultantes pelos seus nomes próprios.

Mas depois

Uma breve descrição de um subconjunto da sintaxe TL para quem não lê a documentação oficial

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;

A definição sempre começa desenhista, após o que opcionalmente (na prática - sempre) através do símbolo # deveria CRC32 da string de descrição normalizada deste tipo. A seguir vem uma descrição dos campos; se existirem, o tipo pode estar vazio. Tudo isso termina com um sinal de igual, o nome do tipo ao qual esse construtor - ou seja, o subtipo - pertence. O cara à direita do sinal de igual é polimórfico - isto é, vários tipos específicos podem corresponder a ele.

Se a definição ocorrer após a linha ---functions---, então a sintaxe permanecerá a mesma, mas o significado será diferente: o construtor se tornará o nome da função RPC, os campos se tornarão parâmetros (bem, isto é, permanecerá exatamente a mesma estrutura dada, conforme descrito abaixo , este será simplesmente o significado atribuído) e o “tipo polimórfico” - o tipo do resultado retornado. É verdade que ainda permanecerá polimórfico - apenas definido na seção ---types---, mas este construtor “não será considerado”. Sobrecarregar os tipos de funções chamadas por seus argumentos, ou seja, Por alguma razão, diversas funções com o mesmo nome, mas assinaturas diferentes, como em C++, não estão previstas na TL.

Por que “construtor” e “polimórfico” se não é OOP? Bem, na verdade, será mais fácil para alguém pensar sobre isso em termos de OOP - um tipo polimórfico como uma classe abstrata, e os construtores são suas classes descendentes diretas, e final na terminologia de vários idiomas. Na verdade, é claro, aqui apenas semelhança com métodos construtores sobrecarregados reais em linguagens de programação OO. Como aqui estão apenas estruturas de dados, não existem métodos (embora a descrição adicional de funções e métodos seja bastante capaz de criar confusão na cabeça de que eles existem, mas isso é uma questão diferente) - você pode pensar em um construtor como um valor de qual está sendo construído digite ao ler um fluxo de bytes.

Como isso acontece? O desserializador, que sempre lê 4 bytes, vê o valor 0xcrc32 - e entende o que acontecerá a seguir field1 com tipo int, ou seja lê exatamente 4 bytes, neste o campo sobreposto com o tipo PolymorType ler. Vê 0x2crc32 e entende que existem dois campos além, primeiro long, o que significa que lemos 8 bytes. E novamente um tipo complexo, que é desserializado da mesma maneira. Por exemplo, Type3 poderiam ser declarados no circuito assim que dois construtores, respectivamente, então eles devem atender 0x12abcd34, após o qual você precisa ler mais 4 bytes intOu 0x6789cdef, após o qual não haverá nada. Qualquer outra coisa - você precisa lançar uma exceção. Enfim, depois disso voltamos a ler 4 bytes int margens field_c в constructorTwo e com isso terminamos de ler nosso PolymorType.

Finalmente, se você for pego 0xdeadcrc para constructorThree, então tudo fica mais complicado. Nosso primeiro campo é bit_flags_of_what_really_present com tipo # - na verdade, este é apenas um apelido para o tipo nat, que significa "número natural". A propósito, isto é, unsigned int é o único caso em que números não assinados ocorrem em circuitos reais. Então, a seguir vem uma construção com um ponto de interrogação, o que significa que este campo - ele estará presente no fio somente se o bit correspondente estiver definido no campo referido (aproximadamente como um operador ternário). Então, vamos supor que este bit foi definido, o que significa que ainda precisamos ler um campo como Type, que em nosso exemplo possui 2 construtores. Um está vazio (consiste apenas no identificador), o outro possui um campo ids com tipo ids:Vector<long>.

Você pode pensar que tanto os modelos quanto os genéricos são profissionais ou Java. Mas não. Quase. Esse apenas caso de uso de colchetes angulares em circuitos reais, e é usado SOMENTE para Vector. No fluxo de bytes, serão 4 bytes CRC32 para o próprio tipo Vector, sempre iguais, depois 4 bytes - o número de elementos da matriz e, a seguir, esses próprios elementos.

Adicione a isso o fato de que a serialização sempre ocorre em palavras de 4 bytes, todos os tipos são múltiplos dela - os tipos integrados também são descritos bytes и string com serialização manual do comprimento e esse alinhamento por 4 - bem, parece normal e até relativamente eficaz? Embora TL seja considerado uma serialização binária eficaz, para o inferno com eles, com a expansão de praticamente qualquer coisa, até mesmo valores booleanos e strings de um único caractere para 4 bytes, o JSON ainda será muito mais espesso? Olha, até campos desnecessários podem ser ignorados com flags de bits, tudo é muito bom e até extensível para o futuro, então por que não adicionar novos campos opcionais ao construtor mais tarde?

Mas não, se você não ler minha breve descrição, mas a documentação completa, e pensar na implementação. Primeiramente, o CRC32 do construtor é calculado de acordo com a linha normalizada da descrição do texto do esquema (remover espaços em branco extras, etc.) - portanto, se um novo campo for adicionado, a linha de descrição do tipo mudará e, portanto, seu CRC32 e , conseqüentemente, serialização. E o que o cliente antigo faria se recebesse um campo com novas flags definidas e não soubesse o que fazer com elas a seguir?

Em segundo lugar, vamos lembrar CRC32, que é usado aqui essencialmente como funções hash para determinar exclusivamente que tipo está sendo (des) serializado. Aqui nos deparamos com o problema das colisões - e não, a probabilidade não é de uma em 232, mas muito maior. Quem se lembrou que o CRC32 foi projetado para detectar (e corrigir) erros no canal de comunicação e, consequentemente, melhorar essas propriedades em detrimento de outras? Por exemplo, ele não se importa em reorganizar bytes: se você calcular CRC32 a partir de duas linhas, na segunda você troca os primeiros 4 bytes pelos próximos 4 bytes - será a mesma coisa. Quando nossa entrada são sequências de texto do alfabeto latino (e um pouco de pontuação), e esses nomes não são particularmente aleatórios, a probabilidade de tal reorganização aumenta muito.

Aliás, quem verificou o que tinha ali? realmente CRC32? Um dos primeiros códigos-fonte (mesmo antes de Waltman) tinha uma função hash que multiplicava cada caractere pelo número 239, tão querido por essas pessoas, ha ha!

Finalmente, ok, percebemos que construtores com tipo de campo Vector<int> и Vector<PolymorType> terá CRC32 diferente. E quanto ao desempenho on-line? E do ponto de vista teórico, isso se torna parte do tipo? Digamos que passamos um array de dez mil números, bem com Vector<int> está tudo claro, o comprimento e mais 40000 bytes. E se isso Vector<Type2>, que consiste em apenas um campo int e está sozinho no tipo - precisamos repetir 10000xabcdef0 34 vezes e depois 4 bytes int, ou a linguagem é capaz de INDEPENDÊ-LA para nós do construtor fixedVec e em vez de 80000 bytes, transferir novamente apenas 40000?

Esta não é uma questão teórica inútil - imagine que você receba uma lista de usuários de um grupo, cada um com um ID, nome e sobrenome - a diferença na quantidade de dados transferidos por uma conexão móvel pode ser significativa. É precisamente a eficácia da serialização do Telegram que nos é anunciada.

Então ...

Vector, que nunca foi lançado

Se você tentar percorrer as páginas de descrição de combinadores e assim por diante, verá que um vetor (e até mesmo uma matriz) está formalmente tentando ser gerado por meio de tuplas de várias folhas. Mas no final eles esquecem, a etapa final é ignorada e simplesmente é dada uma definição de um vetor, que ainda não está vinculado a um tipo. Qual é o problema? Em idiomas programação, especialmente os funcionais, é bastante típico descrever a estrutura recursivamente - o compilador, com sua avaliação preguiçosa, entenderá e fará tudo sozinho. Na linguagem serialização de dados o que é necessário é EFICIÊNCIA: basta simplesmente descrever lista, ou seja estrutura de dois elementos - o primeiro é um elemento de dados, o segundo é a mesma estrutura ou um espaço vazio para a cauda (pacote (cons) em Lisp). Mas isso obviamente exigirá cada O elemento gasta 4 bytes adicionais (CRC32 no caso em TL) para descrever seu tipo. Uma matriz também pode ser facilmente descrita tamanho fixo, mas no caso de uma matriz de comprimento desconhecido antecipadamente, interrompemos.

Portanto, como o TL não permite a saída de um vetor, ele teve que ser adicionado ao lado. Em última análise, a documentação diz:

A serialização sempre usa o mesmo construtor “vetor” (const 0x1cb5c415 = crc32(“vector t:Type # [ t ] = Vector t”) que não depende do valor específico da variável do tipo t.

O valor do parâmetro opcional t não está envolvido na serialização, pois é derivado do tipo de resultado (sempre conhecido antes da desserialização).

Olhe mais de perto: vector {t:Type} # [ t ] = Vector t - mas lugar nenhum Esta definição em si não diz que o primeiro número deve ser igual ao comprimento do vetor! E isso não vem de lugar nenhum. Este é um dado que precisa ser mantido em mente e implementado com as mãos. Em outros lugares, a documentação menciona honestamente que o tipo não é real:

O pseudotipo polimórfico Vector t é um “tipo” cujo valor é uma sequência de valores de qualquer tipo t, seja em caixa ou vazio.

... mas não se concentra nisso. Quando você, cansado de se atrapalhar com a matemática (talvez até conhecida por você de um curso universitário), decide desistir e realmente olha como trabalhar com ela na prática, a impressão que fica na sua cabeça é que isso é sério A matemática em sua essência foi claramente inventada por Cool People (dois matemáticos - vencedor do ACM), e não por qualquer um. O objetivo – exibir-se – foi alcançado.

A propósito, sobre o número. Deixe-nos lembrá-lo que # é um sinônimo nat, número natural:

Existem expressões de tipo (tipo-expr) e expressões numéricas (nat-expr). No entanto, eles são definidos da mesma maneira.

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

mas na gramática eles são descritos da mesma maneira, ou seja, Esta diferença deve novamente ser lembrada e implementada manualmente.

Bem, sim, tipos de modelo (vector<int>, vector<User>) têm um identificador comum (#1cb5c415), ou seja se você sabe que a chamada é anunciada como

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

então você não está mais esperando apenas um vetor, mas um vetor de usuários. Mais precisamente, deveria espere - no código real, cada elemento, se não for do tipo simples, terá um construtor, e no bom sentido na implementação seria necessário verificar - mas fomos enviados exatamente em cada elemento deste vetor esse tipo? E se fosse algum tipo de PHP, em que um array pode conter diferentes tipos em diferentes elementos?

Neste ponto você começa a pensar - esse TL é necessário? Talvez para o carrinho fosse possível usar um serializador humano, o mesmo protobuf que já existia então? Essa era a teoria, vamos ver a prática.

Implementações TL existentes em código

TL nasceu nas profundezas do VKontakte antes mesmo dos famosos acontecimentos com a venda da participação de Durov e (certamente), antes mesmo do início do desenvolvimento do Telegram. E em código aberto código fonte da primeira implementação você pode encontrar muitas muletas engraçadas. E a linguagem em si foi implementada lá de forma mais completa do que agora no Telegram. Por exemplo, hashes não são usados ​​no esquema (ou seja, um pseudotipo integrado (como um vetor) com comportamento desviante). Ou

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

mas consideremos, para completar, traçar, por assim dizer, a evolução do Gigante do Pensamento.

#define ZHUKOV_BYTES_HACK

#ifdef ZHUKOV_BYTES_HACK

/* dirty hack for Zhukov request */

Ou esta linda:

    static const char *reserved_words_polymorhic[] = {

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

      };

Este fragmento é sobre modelos como:

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

Esta é a definição de um tipo de modelo hashmap como um vetor de pares int - Type. Em C++ seria algo assim:

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

então aqui alpha - palavra-chave! Mas só em C++ você pode escrever T, mas você deve escrever alfa, beta... Mas não mais que 8 parâmetros, é aí que a fantasia termina. Parece que era uma vez em São Petersburgo alguns diálogos como este:

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

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

Mas tratava-se da primeira implementação publicada de TL “em geral”. Vamos considerar as implementações nos próprios clientes do Telegram.

Palavra para Vasily:

Vasily, [09.10.18 17:07] Acima de tudo, a bunda é quente porque eles criaram um monte de abstrações e depois martelaram um parafuso nelas e cobriram o gerador de código com muletas
Como resultado, primeiro em dock pilot.jpg
Então, a partir do código dzhekichan.webp

Claro, de pessoas familiarizadas com algoritmos e matemática, podemos esperar que elas tenham lido Aho, Ullmann, e estejam familiarizadas com as ferramentas que se tornaram padrão de fato na indústria ao longo das décadas para escrever seus compiladores DSL, certo?

Pelo autor telegrama-cli é Vitaly Valtman, como pode ser entendido pela ocorrência do formato TLO fora de seus limites (cli), um membro da equipe - agora uma biblioteca para análise de TL foi alocada separadamente, qual é a impressão dela Analisador TL? ..

16.12 04:18 Vasily: Acho que alguém não dominou lex+yacc
16.12 04:18 Vasily: Não posso explicar de outra forma
16.12 04:18 Vasily: bem, ou eles foram pagos pelo número de linhas em VK
16.12 04:19 Vasily: 3k+ linhas etc.<censored> em vez de um analisador

Talvez uma exceção? Vamos ver como faz Este é o cliente OFICIAL - 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);

Mais de 1100 linhas em Python, algumas expressões regulares + casos especiais como um vetor, que, é claro, é declarado no esquema como deveria ser de acordo com a sintaxe TL, mas eles confiaram nessa sintaxe para analisá-lo... Surge a pergunta: por que tudo foi um milagre?иTem mais camadas se ninguém vai analisá-lo de acordo com a documentação?!

A propósito... Lembra que falamos sobre a verificação do CRC32? Assim, no gerador de código Telegram Desktop existe uma lista de exceções para aqueles tipos em que o CRC32 calculado não corresponde com o indicado no diagrama!

Vasily, [18.12/22 49:XNUMX] e aqui eu gostaria de pensar se tal TL é necessário
se eu quisesse mexer com implementações alternativas, começaria a inserir quebras de linha, metade dos analisadores quebraria em definições de várias linhas
tdesktop, no entanto, também

Lembre-se do ponto sobre one-liner, retornaremos a ele um pouco mais tarde.

Ok, o Telegram-cli não é oficial, o Telegram Desktop é oficial, mas e os outros? Quem sabe?.. No código do cliente Android não havia nenhum analisador de esquema (o que levanta questões sobre código aberto, mas isso é para a segunda parte), mas havia vários outros trechos de código engraçados, mas mais sobre eles no subseção abaixo.

Que outras questões a serialização levanta na prática? Por exemplo, eles fizeram muitas coisas, é claro, com campos de bits e campos condicionais:

Basílio: flags.0? true
significa que o campo está presente e é igual a verdadeiro se o sinalizador estiver definido

Basílio: flags.1? int
significa que o campo está presente e precisa ser desserializado

Vasily: Idiota, não se preocupe com o que você está fazendo!
Vasily: Há uma menção em algum lugar no documento que true é um tipo simples de comprimento zero, mas é impossível montar qualquer coisa a partir do documento deles
Vasily: Nas implementações de código aberto este também não é o caso, mas há um monte de muletas e suportes

E o Teleton? Olhando adiante para o tópico do MTProto, um exemplo - na documentação existem tais peças, mas o sinal % é descrito apenas como “correspondente a um determinado tipo nu”, ou seja, nos exemplos abaixo há um erro ou algo não documentado:

Vasily, [22.06.18 18:38] Em um só lugar:

msg_container#73f1f8dc messages:vector message = MessageContainer;

Em um diferente:

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

E essas são duas grandes diferenças, na vida real surge algum tipo de vetor nu

Eu não vi uma definição simples de vetor e não encontrei nenhuma

A análise é escrita à mão no teleton

Em seu diagrama a definição é comentada msg_container

Novamente, a questão permanece sobre %. Não está descrito.

Vadim Goncharov, [22.06.18 19:22] e no tdesktop?

Vasily, [22.06.18 19:23] Mas o analisador TL deles em motores regulares provavelmente também não consumirá isso

// parsed manually

TL é uma bela abstração, ninguém a implementa completamente

E % não está na versão deles do esquema

Mas aqui a documentação se contradiz, então não sei

Foi encontrado na gramática, eles poderiam simplesmente ter esquecido de descrever a semântica

Você viu o documento em TL, não consegue descobrir sem meio litro

“Bem, digamos”, dirá outro leitor, “você critica algo, então mostre-me como isso deve ser feito”.

Vasily responde: “Quanto ao analisador, gosto de coisas como

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

de alguma forma gosto mais do que

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

ou

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

este é o lexer INTEIRO:

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

aqueles. mais simples é dizer o mínimo.

Em geral, como resultado, o analisador e o gerador de código para o subconjunto de TL realmente usado cabem em aproximadamente 100 linhas de gramática e aproximadamente 300 linhas do gerador (contando todas printcódigo gerado por), incluindo informações de tipo para introspecção em cada classe. Cada tipo polimórfico se transforma em uma classe base abstrata vazia, e os construtores herdam dela e possuem métodos para serialização e desserialização.

Falta de tipos na linguagem de tipos

Digitação forte é uma coisa boa, certo? Não, este não é um holivar (embora eu prefira linguagens dinâmicas), mas um postulado no âmbito da TL. Com base nisso, a linguagem deve fornecer todos os tipos de verificações para nós. Bem, tudo bem, talvez não ele mesmo, mas a implementação, mas ele deveria pelo menos descrevê-los. E que tipo de oportunidades queremos?

Em primeiro lugar, restrições. Aqui vemos na documentação para upload de arquivos:

O conteúdo binário do arquivo é então dividido em partes. Todas as peças devem ter o mesmo tamanho ( tamanho_da_peça ) e as seguintes condições devem ser atendidas:

  • part_size % 1024 = 0 (divisível por 1 KB)
  • 524288 % part_size = 0 (512 KB devem ser divisíveis igualmente por part_size)

A última parte não precisa satisfazer estas condições, desde que seu tamanho seja menor que part_size.

Cada parte deve ter um número de sequência, parte_do_arquivo, com valor variando de 0 a 2,999.

Após o arquivo ter sido particionado, você precisa escolher um método para salvá-lo no servidor. Usar upload.saveBigFilePart caso o tamanho total do arquivo seja superior a 10 MB e upload.saveFilePart para arquivos menores.
[…] um dos seguintes erros de entrada de dados pode ser retornado:

  • FILE_PARTS_INVALID — Número de peças inválido. O valor não está entre 1..3000

Alguma dessas coisas está no diagrama? Isso é de alguma forma expressável usando TL? Não. Mas com licença, até o Turbo Pascal do avô foi capaz de descrever os tipos especificados gamas. E ele sabia mais uma coisa, agora mais conhecida como enum - um tipo que consiste em uma enumeração de um número fixo (pequeno) de valores. Em linguagens como C – numérica, observe que até agora falamos apenas de tipos números. Mas também existem arrays, strings... por exemplo, seria legal descrever que essa string só pode conter um número de telefone, certo?

Nada disso está no TL. Mas existe, por exemplo, no JSON Schema. E se alguém argumentar sobre a divisibilidade de 512 KB, que isso ainda precisa ser verificado no código, certifique-se de que o cliente simplesmente não podia enviar um número fora do alcance 1..3000 (e o erro correspondente não poderia ter surgido) teria sido possível, certo?..

A propósito, sobre erros e valores de retorno. Mesmo aqueles que trabalharam com TL ficam com os olhos turvos - não percebemos imediatamente que cada uma função em TL pode realmente retornar não apenas o tipo de retorno descrito, mas também um erro. Mas isso não pode ser deduzido de forma alguma usando a própria TL. Claro, já está claro e não há necessidade de nada na prática (embora na verdade o RPC possa ser feito de diferentes maneiras, voltaremos a isso mais tarde) - mas e a Pureza dos conceitos da Matemática dos Tipos Abstratos do mundo celestial?.. Eu peguei o rebocador - então combine.

E finalmente, e quanto à legibilidade? Bem, aí, em geral, eu gostaria descrição acerte no esquema (no esquema JSON, novamente, está), mas se você já está sobrecarregado com isso, então que tal o lado prático - pelo menos olhando trivialmente as diferenças durante as atualizações? Veja você mesmo em exemplos reais:

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

ou

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

Depende de todos, mas o GitHub, por exemplo, se recusa a destacar alterações dentro de filas tão longas. O jogo “encontre 10 diferenças”, e o que o cérebro vê imediatamente é que o início e o fim em ambos os exemplos são iguais, você precisa ler tediosamente em algum lugar no meio... Na minha opinião, isso não é apenas na teoria, mas puramente visualmente sujo e desleixado.

Aliás, sobre a pureza da teoria. Por que precisamos de campos de bits? Não parece que eles paхнут ruim do ponto de vista da teoria dos tipos? A explicação pode ser vista em versões anteriores do diagrama. No início sim, era assim, para cada espirro criava-se um novo tipo. Esses rudimentos ainda existem nesta forma, por exemplo:

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;

Mas agora imagine, se você tiver 5 campos opcionais na sua estrutura, então você precisará de 32 tipos para todas as opções possíveis. Explosão combinatória. Assim, a pureza cristalina da teoria TL mais uma vez se estilhaçou contra o traseiro de ferro da dura realidade da serialização.

Além disso, em alguns lugares, esses próprios caras violam sua própria tipologia. Por exemplo, no MTProto (próximo capítulo) a resposta pode ser compactada pelo Gzip, está tudo bem - exceto que as camadas e o circuito são violados. Mais uma vez, não foi o RpcResult em si que foi colhido, mas o seu conteúdo. Bem, por que fazer isso?.. Tive que usar uma muleta para que a compressão funcionasse em qualquer lugar.

Ou outro exemplo, uma vez descobrimos um erro - ele foi enviado InputPeerUser ao invés de InputUser. Ou vice-versa. Mas funcionou! Ou seja, o servidor não se importou com o tipo. Como isso pode ser? A resposta pode nos ser dada por fragmentos de código do 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);

Em outras palavras, é aqui que a serialização é feita MANUALMENTE, não código gerado! Talvez o servidor seja implementado de maneira semelhante?.. Em princípio, isso funcionará se for feito uma vez, mas como pode ser suportado posteriormente durante as atualizações? Foi por isso que o esquema foi inventado? E aqui passamos para a próxima pergunta.

Versionamento. Camadas

O motivo pelo qual as versões esquemáticas são chamadas de camadas só pode ser especulado com base na história dos esquemas publicados. Aparentemente, a princípio os autores pensaram que coisas básicas poderiam ser feitas usando o esquema inalterado, e somente quando necessário, para solicitações específicas, indicar que estavam sendo feitas usando uma versão diferente. Em princípio, até uma boa ideia - e o novo será, por assim dizer, “misto”, colocado em camadas sobre o antigo. Mas vamos ver como isso foi feito. É verdade que não consegui olhar desde o início - é engraçado, mas o diagrama da camada base simplesmente não existe. As camadas começaram com 2. A documentação nos fala sobre um recurso especial do TL:

Se um cliente suportar a Camada 2, o seguinte construtor deverá ser usado:

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

Na prática, isso significa que antes de cada chamada de API, um int com o valor 0x289dd1f6 deve ser adicionado antes do número do método.

Parece normal. Mas o que aconteceu a seguir? Então apareceu

invokeWithLayer3#b7475268 query:!X = X;

Então, o que vem a seguir? Como você pode imaginar,

invokeWithLayer4#dea0d430 query:!X = X;

Engraçado? Não, é muito cedo para rir, pense no fato de que cada uma solicitação de outra camada precisa ser agrupada em um tipo especial - se você tiver todos diferentes, de que outra forma poderá distingui-los? E adicionar apenas 4 bytes na frente é um método bastante eficiente. Então,

invokeWithLayer5#417a57ae query:!X = X;

Mas é óbvio que depois de um tempo isso se tornará uma espécie de bacanal. E a solução veio:

Atualização: começando com a camada 9, métodos auxiliares invokeWithLayerN só pode ser usado junto com initConnection

Viva! Depois de 9 versões, finalmente chegamos ao que era feito nos protocolos da Internet na década de 80 - combinar a versão uma vez no início da conexão!

Então, o que vem a seguir?

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

Mas agora você ainda pode rir. Só depois de mais 9 camadas foi finalmente adicionado um construtor universal com um número de versão, que precisa ser chamado apenas uma vez no início da conexão, e o significado das camadas parecia ter desaparecido, agora é apenas uma versão condicional, como em qualquer outro lugar. Problema resolvido.

Exatamente?..

Vasily, [16.07.18 14:01] Ainda na sexta pensei:
O teleserver envia eventos sem solicitação. As solicitações devem ser agrupadas em InvokeWithLayer. O servidor não agrupa atualizações; não há estrutura para agrupar respostas e atualizações.

Aqueles. o cliente não pode especificar a camada na qual deseja atualizações

Vadim Goncharov, [16.07.18 14:02] InvokeWithLayer não é uma muleta em princípio?

Vasily, [16.07.18 14:02] Esta é a única maneira

Vadim Goncharov, [16.07.18 14:02] o que essencialmente deveria significar concordar com a camada no início da sessão

A propósito, segue-se que o downgrade do cliente não é fornecido

Atualizações, ou seja tipo Updates no esquema, é isso que o servidor envia ao cliente não em resposta a uma solicitação da API, mas de forma independente quando ocorre um evento. Este é um tema complexo que será discutido em outro post, mas por enquanto é importante saber que o servidor salva atualizações mesmo quando o cliente está offline.

Assim, se você se recusar a embrulhar cada pacote para indicar sua versão, isso leva logicamente aos seguintes possíveis problemas:

  • o servidor envia atualizações para o cliente antes mesmo de o cliente informar qual versão ele suporta
  • o que devo fazer depois de atualizar o cliente?
  • que garantiasque a opinião do servidor sobre o número da camada não mudará durante o processo?

Você acha que isso é especulação puramente teórica, e na prática isso não pode acontecer, porque o servidor está escrito corretamente (pelo menos foi bem testado)? Ha! Não importa como seja!

Foi exatamente isso que encontramos em agosto. No dia 14 de agosto, surgiram mensagens de que algo estava sendo atualizado nos servidores do Telegram... e depois nos logs:

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.

e, em seguida, vários megabytes de rastreamentos de pilha (bem, ao mesmo tempo, o registro foi corrigido). Afinal, se algo não for reconhecido no seu TL, é binário por assinatura, mais adiante ALL vai, a decodificação se tornará impossível. O que você deve fazer em tal situação?

Bem, a primeira coisa que vem à cabeça de alguém é desconectar e tentar novamente. Não ajudou. Pesquisamos CRC32 no Google - eram objetos do esquema 73, embora tenhamos trabalhado no 82. Observamos cuidadosamente os logs - existem identificadores de dois esquemas diferentes!

Talvez o problema esteja puramente em nosso cliente não oficial? Não, lançamos o Telegram Desktop 1.2.17 (versão fornecida em várias distribuições Linux), ele grava no log de exceções: ID de tipo inesperado MTP #b5223b0f lido em MTPMessageMedia…

Crítica do protocolo e abordagens organizacionais do Telegram. Parte 1, técnica: experiência de escrever um cliente do zero - TL, MT

O Google mostrou que um problema semelhante já havia acontecido com um dos clientes não oficiais, mas então os números de versão e, consequentemente, as suposições eram diferentes...

Então o que deveríamos fazer? Vasily e eu nos separamos: ele tentou atualizar o circuito para 91, resolvi esperar alguns dias e experimentar 73. Ambos os métodos funcionaram, mas como são empíricos, não há entendimento de quantas versões para cima ou para baixo você precisa pular ou quanto tempo você precisa esperar.

Mais tarde consegui reproduzir a situação: iniciamos o cliente, desligamos, recompilamos o circuito para outra camada, reiniciamos, pegamos o problema novamente, voltamos para a anterior - opa, não há troca de circuito e o cliente reinicia por um alguns minutos ajudarão. Você receberá uma combinação de estruturas de dados de diferentes camadas.

Explicação? Como você pode imaginar por vários sintomas indiretos, o servidor consiste em muitos processos de tipos diferentes em máquinas diferentes. Muito provavelmente, o servidor responsável pelo “buffering” colocou na fila o que seus superiores lhe deram, e eles deram no esquema que estava em vigor no momento da geração. E até essa fila “podre”, nada poderia ser feito a respeito.

Talvez... mas isso é uma muleta terrível?!.. Não, antes de pensar em ideias malucas, vamos dar uma olhada no código dos clientes oficiais. Na versão Android não encontramos nenhum analisador TL, mas encontramos um arquivo pesado (o GitHub se recusa a retocá-lo) com (des)serialização. Aqui estão os trechos de código:

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;

ou

    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... parece selvagem. Mas, provavelmente, isso é código gerado, então ok?.. Mas certamente suporta todas as versões! É verdade que não está claro por que tudo está misturado, bate-papos secretos e todo tipo de _old7 de alguma forma, não parece geração de máquina... No entanto, acima de tudo, fiquei impressionado com

TL_message_layer104
TL_message_layer104_2
TL_message_layer104_3

Pessoal, vocês não conseguem nem decidir o que tem dentro de uma camada?! Bem, ok, digamos que “dois” foram lançados com erro, bem, isso acontece, mas TRÊS?.. Imediatamente, o mesmo rake de novo? Que tipo de pornografia é essa, desculpe?

A propósito, no código-fonte do Telegram Desktop acontece algo semelhante - nesse caso, vários commits consecutivos no esquema não alteram seu número de camada, mas corrigem alguma coisa. Nas condições em que não existe uma fonte oficial de dados para o esquema, onde podem ser obtidos, exceto o código-fonte do cliente oficial? E se você partir daí, não poderá ter certeza de que o esquema está completamente correto até testar todos os métodos.

Como isso pode ser testado? Espero que os fãs de testes unitários, funcionais e outros compartilhem nos comentários.

Ok, vamos dar uma olhada em outro trecho de código:

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;

Este comentário “criado manualmente” sugere que apenas parte deste arquivo foi escrita manualmente (você pode imaginar todo o pesadelo da manutenção?), e o restante foi gerado por máquina. No entanto, surge outra questão - se as fontes estão disponíveis não completamente (a la GPL blobs no kernel Linux), mas isso já é um tópico para a segunda parte.

Mas chega. Vamos passar para o protocolo sobre o qual toda essa serialização é executada.

MT Proto

Então, vamos abrir descrição geral и descrição detalhada do protocolo e a primeira coisa em que tropeçamos é a terminologia. E com abundância de tudo. Em geral, este parece ser um recurso proprietário do Telegram - chamar coisas de maneira diferente em lugares diferentes, ou coisas diferentes com uma palavra, ou vice-versa (por exemplo, em uma API de alto nível, se você vir um pacote de adesivos, não é o que você pensou).

Por exemplo, “mensagem” e “sessão” significam algo diferente aqui do que na interface normal do cliente Telegram. Bem, tudo fica claro com a mensagem, ela pode ser interpretada em termos OOP, ou simplesmente chamada de palavra “pacote” - este é um nível de transporte baixo, não existem as mesmas mensagens que na interface, existem muitas mensagens de serviço . Mas a sessão... mas primeiro o mais importante.

camada de transporte

A primeira coisa é o transporte. Eles nos contarão cerca de 5 opções:

  • TCP
  • soquete da web
  • Websocket sobre HTTPS
  • HTTP
  • HTTPS

Vasily, [15.06.18 15:04] Também existe transporte UDP, mas não está documentado

E TCP em três variantes

O primeiro é semelhante ao UDP sobre TCP, cada pacote inclui um número de sequência e crc
Por que ler documentos em um carrinho é tão doloroso?

Bem, aí está agora TCP já em 4 variantes:

  • Resumido
  • Nível intermediário
  • Intermediário acolchoado
  • completo

Bem, ok, intermediário acolchoado para MTProxy, isso foi adicionado posteriormente devido a eventos bem conhecidos. Mas por que mais duas versões (três no total) quando você poderia sobreviver com uma? Todos os quatro diferem essencialmente apenas em como definir o comprimento e a carga útil do MTProto principal, que será discutido mais adiante:

  • em Abridged é 1 ou 4 bytes, mas não 0xef, então o corpo
  • no Intermediário são 4 bytes de comprimento e um campo, e na primeira vez o cliente deve enviar 0xeeeeeeee para indicar que é intermediário
  • na íntegra o mais viciante, do ponto de vista de um networker: comprimento, número de sequência, e NÃO AQUELE que é principalmente MTProto, corpo, CRC32. Sim, tudo isso está em cima do TCP. O que nos fornece um transporte confiável na forma de um fluxo de bytes sequencial; nenhuma sequência é necessária, especialmente somas de verificação. Ok, agora alguém vai me objetar que o TCP tem uma soma de verificação de 16 bits, então ocorre corrupção de dados. Ótimo, mas na verdade temos um protocolo criptográfico com hashes maiores que 16 bytes, todos esses erros - e ainda mais - serão detectados por uma incompatibilidade de SHA em um nível superior. Não há nenhum ponto no CRC32 além disso.

Vamos comparar o Abridged, no qual é possível um byte de comprimento, com o Intermediate, que justifica “Caso seja necessário alinhamento de dados de 4 bytes”, o que é um absurdo. Acredita-se que os programadores do Telegram são tão incompetentes que não conseguem ler dados de um soquete em um buffer alinhado? Você ainda precisa fazer isso, porque a leitura pode retornar qualquer número de bytes (e também existem servidores proxy, por exemplo...). Ou, por outro lado, por que bloquear Abridged se ainda teremos um preenchimento robusto além de 16 bytes - economize 3 bytes às vezes ?

Tem-se a impressão de que Nikolai Durov gosta muito de reinventar rodas, incluindo protocolos de rede, sem qualquer necessidade prática real.

Outras opções de transporte, incl. Web e MTProxy, não vamos considerar agora, talvez em outro post, caso haja solicitação. Sobre este mesmo MTProxy, lembremos apenas agora que logo após seu lançamento em 2018, os provedores aprenderam rapidamente a bloqueá-lo, destinado a ignorar bloqueioPor tamanho do pacote! E também o fato de que o servidor MTProxy escrito (novamente por Waltman) em C estava excessivamente vinculado às especificidades do Linux, embora isso não fosse necessário (Phil Kulin confirmará), e que um servidor semelhante em Go ou Node.js seria cabem em menos de cem linhas.

Mas tiraremos conclusões sobre a literacia técnica destas pessoas no final da secção, depois de considerar outras questões. Por enquanto, vamos passar para a camada 5 do OSI, sessão - na qual eles colocaram a sessão MTProto.

Chaves, mensagens, sessões, Diffie-Hellman

Eles a colocaram lá de forma incorreta... Uma sessão não é a mesma sessão que está visível na interface em Sessões ativas. Mas em ordem.

Crítica do protocolo e abordagens organizacionais do Telegram. Parte 1, técnica: experiência de escrever um cliente do zero - TL, MT

Portanto, recebemos uma sequência de bytes de comprimento conhecido da camada de transporte. Esta é uma mensagem criptografada ou texto simples - se ainda estivermos na fase de acordo chave e estivermos realmente fazendo isso. De qual grupo de conceitos chamados “chave” estamos falando? Vamos esclarecer essa questão para a própria equipe do Telegram (peço desculpas por traduzir minha própria documentação do inglês com o cérebro cansado às 4 da manhã, foi mais fácil deixar algumas frases como estão):

Existem duas entidades chamadas Sessão - um na UI dos clientes oficiais em “sessões atuais”, onde cada sessão corresponde a um dispositivo/SO inteiro.
O segundo - Sessão MTProto, que contém o número de sequência da mensagem (em um sentido de baixo nível) e que pode durar entre diferentes conexões TCP. Várias sessões MTProto podem ser instaladas ao mesmo tempo, por exemplo, para acelerar o download de arquivos.

Entre esses dois sessões existe um conceito autorização. No caso degenerado, podemos dizer que Sessão de IU é o mesmo que autorização, mas, infelizmente, tudo é complicado. Vamos olhar:

  • O usuário no novo dispositivo primeiro gera chave de autenticação e vincula-o à conta, por exemplo via SMS - é por isso autorização
  • Aconteceu dentro do primeiro Sessão MTProto, que tem session_id dentro de você.
  • Nesta etapa, a combinação autorização и session_id poderia ser chamado instância - esta palavra aparece na documentação e no código de alguns clientes
  • Então, o cliente pode abrir alguns Sessões MTProto sob o mesmo chave de autenticação - para o mesmo DC.
  • Então, um dia o cliente precisará solicitar o arquivo de outro CD - e para esse DC será gerado um novo chave de autenticação !
  • Para informar ao sistema que não é um novo usuário que está se cadastrando, mas sim o mesmo autorização (Sessão de IU), o cliente usa chamadas de API auth.exportAuthorization em casa DC auth.importAuthorization no novo CD.
  • Tudo é igual, vários podem estar abertos Sessões MTProto (cada um com seu próprio session_id) para este novo DC, sob sua chave de autenticação.
  • Finalmente, o cliente pode querer o Perfect Forward Secrecy. Todo chave de autenticação era permanente chave - por DC - e o cliente pode ligar auth.bindTempAuthKey para uso temporário chave de autenticação - e novamente, apenas um temp_auth_key por DC, comum a todos Sessões MTProto para este CD.

Note que sal (e sais futuros) também é um dos chave de autenticação aqueles. compartilhado entre todos Sessões MTProto para o mesmo DC.

O que significa "entre diferentes conexões TCP"? Então isso significa algo como cookie de autorização em um site - ele persiste (sobrevive) a muitas conexões TCP para um determinado servidor, mas um dia estraga. Só que diferentemente do HTTP, no MTProto as mensagens dentro de uma sessão são numeradas sequencialmente e confirmadas; se entraram no túnel, a conexão foi interrompida - após estabelecer uma nova conexão, o servidor gentilmente enviará nesta sessão tudo o que não entregou na anterior Conexão TCP.

No entanto, as informações acima são resumidas após muitos meses de investigação. Enquanto isso, estamos implementando nosso cliente do zero? - vamos voltar ao início.

Então vamos gerar auth_key em Versões Diffie-Hellman do Telegram. Vamos tentar entender a documentação...

Vasily, [19.06.18 20:05] data_with_hash := SHA1(dados) + dados + (quaisquer bytes aleatórios); tal que o comprimento seja igual a 255 bytes;
dados_criptografados: = RSA (dados_com_hash, server_public_key); um número longo de 255 bytes (big endian) é elevado à potência necessária sobre o módulo necessário e o resultado é armazenado como um número de 256 bytes.

Eles têm alguma droga DH

Não se parece com o DH de uma pessoa saudável
Não existem duas chaves públicas em dx

Bom, no final isso foi resolvido, mas ficou um resíduo - a prova do trabalho é feita pelo cliente de que ele conseguiu fatorar o número. Tipo de proteção contra ataques DoS. E a chave RSA é usada apenas uma vez em uma direção, essencialmente para criptografia new_nonce. Mas embora esta operação aparentemente simples seja bem-sucedida, o que você terá que enfrentar?

Vasily, [20.06.18/00/26 XNUMX:XNUMX] Ainda não cheguei à solicitação do appid

Enviei esse pedido para DH

E, na doca de transporte diz que pode responder com 4 bytes de um código de erro. Isso é tudo

Bem, ele me disse -404, e daí?

Então eu disse a ele: “Pegue sua besteira criptografada com uma chave de servidor com uma impressão digital como esta, quero DH”, e ele respondeu com um estúpido 404

O que você acha desta resposta do servidor? O que fazer? Não há ninguém a quem perguntar (mas falaremos mais sobre isso na segunda parte).

Aqui todo o interesse é feito no cais

Não tenho mais nada para fazer, apenas sonhei em converter números para frente e para trás

Dois números de 32 bits. Eu os empacotei como todo mundo

Mas não, esses dois precisam ser adicionados primeiro à linha como BE

Vadim Goncharov, [20.06.18 15:49] e por causa disso 404?

Vasily, [20.06.18 15:49] SIM!

Vadim Goncharov, [20.06.18 15:50] então não entendo o que ele pode “não encontrar”

Basílio, [20.06.18 15:50] sobre

Não consegui encontrar tal decomposição em fatores primos%)

Nós nem gerenciamos relatórios de erros

Vasily, [20.06.18 20:18] Ah, também tem MD5. Já três hashes diferentes

A impressão digital da chave é calculada da seguinte forma:

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

SHA1 e sha2

Então vamos colocar isso auth_key recebemos 2048 bits de tamanho usando Diffie-Hellman. Qual é o próximo? A seguir descobrimos que os 1024 bits inferiores desta chave não são usados ​​de forma alguma... mas vamos pensar sobre isso por enquanto. Nesta etapa, temos um segredo compartilhado com o servidor. Foi instalado um análogo da sessão TLS, que é um procedimento muito caro. Mas o servidor ainda não sabe nada sobre quem somos! Ainda não, na verdade. autorização. Aqueles. se você pensou em termos de “senha de login”, como já fez no ICQ, ou pelo menos “chave de login”, como no SSH (por exemplo, em algum gitlab/github). Recebemos um anônimo. E se o servidor nos disser “esses números de telefone são atendidos por outro DC”? Ou mesmo “seu número de telefone está banido”? O melhor que podemos fazer é guardar a chave na esperança de que ela seja útil e não apodreça até lá.

Aliás, “recebemos” com ressalvas. Por exemplo, confiamos no servidor? E se for falso? Seriam necessárias verificações criptográficas:

Vasily, [21.06.18 17:53] Eles oferecem aos clientes móveis a verificação de primalidade de um número de 2 kbit%)

Mas não está nada claro, nafeijoa

Vasily, [21.06.18 18:02] O documento não diz o que fazer se não for simples

Não foi dito. Vamos ver o que o cliente oficial do Android faz neste caso? A é isso que (e sim, o arquivo inteiro é interessante) - como dizem, vou deixar isso aqui:

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

Não, claro que ainda está lá alguns Existem testes para a primalidade de um número, mas pessoalmente não tenho mais conhecimentos suficientes de matemática.

Ok, temos a chave mestra. Para fazer login, ou seja, enviar solicitações, você precisará realizar criptografia adicional, usando AES.

A chave da mensagem é definida como os 128 bits intermediários do SHA256 do corpo da mensagem (incluindo sessão, ID da mensagem, etc.), incluindo os bytes de preenchimento, prefixados por 32 bytes retirados da chave de autorização.

Vasily, [22.06.18 14:08] Média, vadia, bits

Recebido auth_key. Todos. Além deles... não está claro no documento. Sinta-se à vontade para estudar o código-fonte aberto.

Observe que o MTProto 2.0 requer de 12 a 1024 bytes de preenchimento, ainda sujeito à condição de que o comprimento da mensagem resultante seja divisível por 16 bytes.

Então, quanto preenchimento você deve adicionar?

E sim, também existe um 404 em caso de erro

Se alguém estudou cuidadosamente o diagrama e o texto da documentação, notou que não há MAC ali. E esse AES é usado em um determinado modo IGE que não é usado em nenhum outro lugar. Eles, é claro, escrevem sobre isso em seu FAQ... Aqui, tipo, a própria chave da mensagem também é o hash SHA dos dados descriptografados, usado para verificar a integridade - e em caso de incompatibilidade, a documentação por algum motivo recomenda ignorá-los silenciosamente (mas e quanto à segurança, e se eles nos quebrarem?).

Não sou criptógrafo, talvez não haja nada de errado com este modo neste caso do ponto de vista teórico. Mas posso citar claramente um problema prático, usando o Telegram Desktop como exemplo. Ele criptografa o cache local (todos esses D877F783D5D3EF8C) da mesma forma que as mensagens no MTProto (somente neste caso a versão 1.0), ou seja, primeiro a chave da mensagem, depois os dados em si (e em algum lugar além do grande auth_key 256 bytes, sem os quais msg_key inútil). Portanto, o problema torna-se perceptível em arquivos grandes. Ou seja, você precisa manter duas cópias dos dados – criptografados e descriptografados. E se houver megabytes, ou streaming de vídeo, por exemplo?.. Esquemas clássicos com MAC após o texto cifrado permitem que você leia o fluxo, transmitindo-o imediatamente. Mas com MTProto você terá que primeiro criptografar ou descriptografar a mensagem inteira e só então transferi-la para a rede ou para o disco. Portanto, nas versões mais recentes do Telegram Desktop no cache em user_data Outro formato também é usado - com AES no modo CTR.

Vasily, [21.06.18 01:27] Ah, descobri o que é IGE: IGE foi a primeira tentativa de um “modo de criptografia de autenticação”, originalmente para Kerberos. Foi uma tentativa fracassada (não fornece proteção de integridade) e teve que ser removida. Esse foi o início de uma busca de 20 anos por um modo de criptografia de autenticação que funcionasse, que recentemente culminou em modos como OCB e GCM.

E agora os argumentos do lado do carrinho:

A equipe por trás do Telegram, liderada por Nikolai Durov, é composta por seis campeões do ACM, metade deles doutores em matemática. Demorou cerca de dois anos para lançar a versão atual do MTProto.

É engraçado. Dois anos no nível inferior

Ou você pode simplesmente pegar tls

Ok, digamos que fizemos a criptografia e outras nuances. Finalmente é possível enviar solicitações serializadas em TL e desserializar as respostas? Então, o que e como você deve enviar? Aqui, digamos, o método conexão inicial, talvez seja isso?

Vasily, [25.06.18 18:46] Inicializa a conexão e salva informações no dispositivo e aplicativo do usuário.

Aceita app_id, device_model, system_version, app_version e lang_code.

E alguma consulta

Documentação como sempre. Sinta-se à vontade para estudar o código aberto

Se tudo ficou aproximadamente claro com InvokeWithLayer, então o que há de errado aqui? Acontece que, digamos que temos - o cliente já tinha algo para perguntar ao servidor - há uma solicitação que queríamos enviar:

Vasily, [25.06.18 19:13] A julgar pelo código, a primeira chamada está envolvida nessa porcaria, e a porcaria em si está envolvida em invokewithlayer

Por que initConnection não poderia ser uma chamada separada, mas deveria ser um wrapper? Sim, como se viu, isso deve ser feito sempre no início de cada sessão, e não uma vez, como acontece com a chave principal. Mas! Não pode ser chamado por um usuário não autorizado! Agora chegamos ao estágio em que é aplicável esse aqui página de documentação - e nos diz que...

Apenas uma pequena parte dos métodos da API está disponível para usuários não autorizados:

  • auth.sendCode
  • auth.resendCode
  • conta.getPassword
  • auth.checkPassword
  • auth.checkPhone
  • auth.signUp
  • auth.signIn
  • auth.importAutorização
  • help.getConfig
  • help.getNearestDc
  • help.getAppUpdate
  • help.getCdnConfig
  • langpack.getLangPack
  • langpack.getStrings
  • langpack.getDifference
  • langpack.getIdiomas
  • langpack.getLanguage

O primeiro deles, auth.sendCode, e há aquela querida primeira solicitação em que enviamos api_id e api_hash, e depois recebemos um SMS com um código. E se estivermos no CD errado (os números de telefone deste país são atendidos por outro, por exemplo), receberemos um erro com o número do CD desejado. Para descobrir a qual endereço IP por número DC você precisa se conectar, ajude-nos help.getConfig. Ao mesmo tempo, havia apenas 5 inscrições, mas depois dos famosos acontecimentos de 2018, o número aumentou significativamente.

Agora vamos lembrar que chegamos a esse estágio no servidor de forma anônima. Não é muito caro obter apenas um endereço IP? Por que não fazer isso e outras operações na parte não criptografada do MTProto? Ouço a objeção: “como podemos ter certeza de que não será a RKN quem responderá com endereços falsos?” Para isso lembramos que, em geral, os clientes oficiais As chaves RSA estão incorporadas, ou seja você pode apenas sinal Essa informação. Na verdade, isso já está sendo feito para obter informações sobre como contornar o bloqueio que os clientes recebem por outros canais (logicamente, isso não pode ser feito no próprio MTProto; você também precisa saber onde se conectar).

OK. Nesta fase de autorização do cliente, ainda não estamos autorizados e não registamos a nossa aplicação. Queremos apenas ver por enquanto o que o servidor responde aos métodos disponíveis para um usuário não autorizado. E aqui…

Basílio, [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;

No esquema, o primeiro vem em segundo

No esquema tdesktop o terceiro valor é

Sim, desde então, claro, a documentação foi atualizada. Embora em breve possa se tornar irrelevante novamente. Como um desenvolvedor novato deve saber? Talvez se você registrar seu aplicativo, eles o informarão? Vasily fez isso, mas, infelizmente, eles não enviaram nada para ele (de novo, falaremos sobre isso na segunda parte).

...Você notou que já mudamos de alguma forma para a API, ou seja, para o próximo nível e perdeu alguma coisa no tópico MTProto? Nenhuma surpresa:

Vasily, [28.06.18 02:04] Hum, eles estão vasculhando alguns dos algoritmos do e2e

Mtproto define algoritmos de criptografia e chaves para ambos os domínios, bem como uma pequena estrutura de wrapper

Mas eles misturam constantemente diferentes níveis da pilha, por isso nem sempre fica claro onde o mtproto termina e o próximo nível começa

Como eles se misturam? Bem, aqui está a mesma chave temporária para PFS, por exemplo (a propósito, o Telegram Desktop não pode fazer isso). É executado por uma solicitação de API auth.bindTempAuthKey, ou seja do nível superior. Mas, ao mesmo tempo, interfere na criptografia de nível inferior - depois disso, por exemplo, você precisa fazer isso novamente initConnection etc, isso não é justo solicitação normal. O que também é especial é que você pode ter apenas UMA chave temporária por DC, embora o campo auth_key_id em cada mensagem permite alterar a chave de pelo menos todas as mensagens, e que o servidor tem o direito de “esquecer” a chave temporária a qualquer momento - a documentação não diz o que fazer neste caso... bem, por que poderia você não tem várias chaves, como acontece com um conjunto de sais futuros, e?..

Há algumas outras coisas dignas de nota sobre o tema MTProto.

Mensagens de mensagens, msg_id, msg_seqno, confirmações, pings na direção errada e outras idiossincrasias

Por que você precisa saber sobre eles? Porque eles “vazam” para um nível superior e você precisa estar atento a eles ao trabalhar com a API. Vamos supor que não estamos interessados ​​em msg_key; o nível inferior descriptografou tudo para nós. Mas dentro dos dados descriptografados temos os seguintes campos (também o comprimento dos dados, para sabermos onde está o preenchimento, mas isso não é importante):

  • sal - int64
  • ID_da_sessão - int64
  • mensagem_id - int64
  • seq_no - int32

Lembramos que existe apenas um sal para todo o DC. Por que saber sobre ela? Não só porque há um pedido get_future_salts, que informa quais intervalos serão válidos, mas também porque se o seu sal estiver “podre”, a mensagem (solicitação) simplesmente será perdida. O servidor irá, é claro, reportar o novo salt emitindo new_session_created - mas com o antigo você terá que reenviá-lo de alguma forma, por exemplo. E esse problema afeta a arquitetura do aplicativo.

O servidor pode interromper completamente as sessões e responder dessa maneira por vários motivos. Na verdade, o que é uma sessão MTProto do lado do cliente? Estes são dois números session_id и seq_no mensagens nesta sessão. Bem, e a conexão TCP subjacente, é claro. Digamos que nosso cliente ainda não sabe fazer muitas coisas, ele desconectou e reconectou. Se isso aconteceu rapidamente - a sessão antiga continuou na nova conexão TCP, aumente seq_no avançar. Se demorar muito, o servidor pode excluí-lo, pois do seu lado também há uma fila, como descobrimos.

O que deveria ser seq_no? Ah, essa é uma pergunta complicada. Tente entender honestamente o que isso significava:

Mensagem relacionada ao conteúdo

Uma mensagem que requer uma confirmação explícita. Estas incluem todas as mensagens do usuário e muitas mensagens de serviço, praticamente todas com exceção de contêineres e confirmações.

Número de sequência de mensagens (msg_seqno)

Um número de 32 bits igual a duas vezes o número de mensagens “relacionadas ao conteúdo” (aquelas que requerem confirmação e, em particular, aquelas que não são contêineres) criadas pelo remetente antes desta mensagem e subsequentemente incrementadas em um se a mensagem atual for um mensagem relacionada ao conteúdo. Um contêiner é sempre gerado após todo o seu conteúdo; portanto, seu número de sequência é maior ou igual aos números de sequência das mensagens nele contidas.

Que tipo de circo é esse com um incremento de 1 e depois outro de 2?.. Suspeito que inicialmente eles queriam dizer “o bit menos significativo para ACK, o resto é um número”, mas o resultado não é exatamente o mesmo - em particular, sai, pode ser enviado alguns confirmações com o mesmo seq_no! Como? Bem, por exemplo, o servidor nos envia algo, envia, e nós mesmos ficamos em silêncio, respondendo apenas com mensagens de serviço confirmando o recebimento de suas mensagens. Neste caso, nossas confirmações de saída terão o mesmo número de saída. Se você está familiarizado com o TCP e acha que isso parece estranho, mas não parece muito estranho, porque no TCP seq_no não muda, mas a confirmação vai para seq_no por outro lado, vou me apressar em incomodá-lo. As confirmações são fornecidas no MTProto NÃO em seq_no, como no TCP, mas por msg_id !

O que é isso msg_id, o mais importante desses campos? Um identificador de mensagem exclusivo, como o nome sugere. É definido como um número de 64 bits, cujos bits mais baixos têm novamente a magia “servidor-não-servidor”, e o resto é um carimbo de data/hora Unix, incluindo a parte fracionária, deslocado 32 bits para a esquerda. Aqueles. timestamp por si só (e mensagens com horários muito diferentes serão rejeitadas pelo servidor). A partir disso verifica-se que em geral este é um identificador global para o cliente. Dado isso - vamos lembrar session_id - temos garantia: Sob nenhuma circunstância uma mensagem destinada a uma sessão pode ser enviada para uma sessão diferente. Ou seja, acontece que já existe três nível - sessão, número da sessão, ID da mensagem. Por que tanta complicação, esse mistério é muito grande.

Assim, msg_id necessário para...

RPC: solicitações, respostas, erros. Confirmações.

Como você deve ter notado, não existe um tipo ou função especial de “fazer uma solicitação RPC” em nenhum lugar do diagrama, embora existam respostas. Afinal, temos mensagens relacionadas ao conteúdo! Aquilo é, qualquer a mensagem pode ser um pedido! Ou não ser. Afinal, cada tem msg_id. Mas há respostas:

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

É aqui que é indicado a qual mensagem esta é uma resposta. Portanto, no nível superior da API, você terá que lembrar qual era o número da sua solicitação - acho que não há necessidade de explicar que o trabalho é assíncrono e pode haver várias solicitações em andamento ao mesmo tempo, cujas respostas podem ser retornadas em qualquer ordem? Em princípio, a partir disso e de mensagens de erro como nenhum trabalhador, a arquitetura por trás disso pode ser rastreada: o servidor que mantém uma conexão TCP com você é um balanceador de front-end, ele encaminha solicitações para os back-ends e as coleta de volta via message_id. Parece que tudo aqui é claro, lógico e bom.

Sim?.. E se você pensar bem? Afinal, a própria resposta RPC também possui um campo msg_id! Precisamos gritar com o servidor “você não está respondendo minha resposta!”? E sim, o que houve nas confirmações? Sobre a página mensagens sobre mensagens nos diz o que é

msgs_ack#62d6b459 msg_ids:Vector long = MsgsAck;

e isso deve ser feito por cada lado. Mas não sempre! Se você recebeu um RpcResult, ele serve como uma confirmação. Ou seja, o servidor pode responder à sua solicitação com MsgsAck - como “Recebi”. RpcResult pode responder imediatamente. Poderia ser ambos.

E sim, você ainda precisa responder! Confirmação. Caso contrário, o servidor irá considerá-lo não entregue e enviá-lo de volta para você novamente. Mesmo após a reconexão. Mas aqui, é claro, surge a questão dos tempos limite. Vamos examiná-los um pouco mais tarde.

Enquanto isso, vejamos possíveis erros de execução de consulta.

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

Ah, alguém vai exclamar, aqui está um formato mais humano - tem fila! Sem pressa. Aqui lista de erros, mas é claro que não está completo. Com isso aprendemos que o código é algo como Erros HTTP (bem, claro, a semântica das respostas não é respeitada, em alguns lugares elas são distribuídas aleatoriamente entre os códigos), e a linha fica assim CAPITAL_LETTERS_AND_NUMBERS. Por exemplo, PHONE_NUMBER_OCCUPIED ou FILE_PART_Х_MISSING. Bem, isto é, você ainda precisará desta linha analisar. Por exemplo, FLOOD_WAIT_3600 significará que você terá que esperar uma hora, e PHONE_MIGRATE_5, que um número de telefone com esse prefixo deverá ser cadastrado no 5º DC. Temos uma linguagem de tipos, certo? Não precisamos de um argumento de uma string, os normais servirão, ok.

Novamente, isso não está na página de mensagens de serviço, mas, como já é habitual neste projeto, as informações podem ser encontradas em outra página de documentação. Ou lançar suspeita. Em primeiro lugar, veja, violação de digitação/camada - RpcError pode ser aninhado RpcResult. Por que não lá fora? O que não levamos em conta?.. Nesse sentido, onde está a garantia de que RpcError NÃO pode ser incorporado em RpcResult, mas estar diretamente ou aninhado em outro tipo?.. E se não puder, por que não está no nível superior, ou seja, está faltando req_msg_id ? ..

Mas vamos continuar com as mensagens de serviço. O cliente pode pensar que o servidor está pensando há muito tempo e fazer esta solicitação maravilhosa:

rpc_drop_answer#58e4a740 req_msg_id:long = RpcDropAnswer;

Existem três respostas possíveis para esta questão, cruzando novamente com o mecanismo de confirmação; tentar compreender o que deveriam ser (e qual a lista geral de tipos que não necessitam de confirmação) fica ao leitor como trabalho de casa (nota: a informação em o código-fonte do Telegram Desktop não está completo).

Dependência de drogas: status de mensagens

Em geral, muitos lugares em TL, MTProto e Telegram em geral deixam um sentimento de teimosia, mas por educação, tato e outros habilidades interpessoais Nós educadamente mantivemos silêncio sobre isso e censuramos as obscenidades nos diálogos. No entanto, este lugarОa maior parte da página é sobre mensagens sobre mensagens É chocante até para mim, que trabalho com protocolos de rede há muito tempo e já vi bicicletas com vários graus de tortuosidade.

Começa de forma inócua, com confirmações. A seguir eles nos contam sobre

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;

Bom, todo mundo que começar a trabalhar com MTProto terá que lidar com eles; no ciclo “corrigido - recompilado - lançado”, é comum receber erros de número ou sal que deu errado durante as edições. No entanto, existem dois pontos aqui:

  1. Isso significa que a mensagem original foi perdida. Precisamos criar algumas filas, veremos isso mais tarde.
  2. Quais são esses números de erro estranhos? 16, 17, 18, 19, 20, 32, 33, 34, 35, 48, 64... onde estão os outros números, Tommy?

A documentação afirma:

A intenção é que os valores de error_code sejam agrupados (error_code >> 4): por exemplo, os códigos 0x40 — 0x4f correspondem a erros na decomposição do container.

mas, em primeiro lugar, uma mudança na outra direção e, em segundo lugar, não importa, onde estão os outros códigos? Na cabeça do autor?.. Porém, são ninharias.

O vício começa nas mensagens sobre status e cópias das mensagens:

  • Solicitação de informações de status de mensagem
    Se uma das partes não receber informações sobre o status de suas mensagens enviadas por algum tempo, poderá solicitá-las explicitamente à outra parte:
    msgs_state_req#da69fb52 msg_ids:Vector long = MsgsStateReq;
  • Mensagem informativa sobre status de mensagens
    msgs_state_info#04deb57d req_msg_id:long info:string = MsgsStateInfo;
    Aqui, info é uma string que contém exatamente um byte de status da mensagem para cada mensagem da lista msg_ids recebida:

    • 1 = nada se sabe sobre a mensagem (msg_id muito baixo, a outra parte pode ter esquecido)
    • 2 = mensagem não recebida (msg_id está dentro do intervalo de identificadores armazenados; no entanto, a outra parte certamente não recebeu uma mensagem como essa)
    • 3 = mensagem não recebida (msg_id muito alto; no entanto, a outra parte certamente ainda não a recebeu)
    • 4 = mensagem recebida (observe que esta resposta também é ao mesmo tempo uma confirmação de recebimento)
    • +8 = mensagem já reconhecida
    • +16 = mensagem que não requer confirmação
    • +32 = Consulta RPC contida na mensagem sendo processada ou processamento já concluído
    • +64 = resposta relacionada ao conteúdo à mensagem já gerada
    • +128 = a outra parte sabe com certeza que a mensagem já foi recebida
      Esta resposta não requer reconhecimento. É um reconhecimento do msgs_state_req relevante, por si só.
      Observe que se, de repente, a outra parte não tiver uma mensagem que pareça ter sido enviada, a mensagem poderá simplesmente ser reenviada. Mesmo que a outra parte receba duas cópias da mensagem ao mesmo tempo, a duplicata será ignorada. (Se tiver passado muito tempo e o msg_id original não for mais válido, a mensagem deverá ser agrupada em msg_copy).
  • Comunicação Voluntária de Status de Mensagens
    Qualquer uma das partes pode informar voluntariamente a outra parte sobre o estado das mensagens transmitidas pela outra parte.
    msgs_all_info#8cc0d131 msg_ids:Vector long info:string = MsgsAllInfo
  • Comunicação voluntária estendida do status de uma mensagem
    ...
    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;
  • Solicitação explícita para reenviar mensagens
    msg_resend_req#7d861a08 msg_ids:Vector long = MsgResendReq;
    A parte remota responde imediatamente reenviando as mensagens solicitadas [...]
  • Solicitação explícita para reenviar respostas
    msg_resend_ans_req#8610baeb msg_ids:Vector long = MsgResendReq;
    A parte remota responde imediatamente reenviando respostas às mensagens solicitadas […]
  • Cópias de mensagens
    Em algumas situações, uma mensagem antiga com um msg_id que não é mais válido precisa ser reenviada. Em seguida, ele é embalado em um contêiner de cópia:
    msg_copy#e06046b2 orig_message:Message = MessageCopy;
    Uma vez recebida, a mensagem é processada como se o wrapper não existisse. No entanto, se for conhecido com certeza que a mensagem orig_message.msg_id foi recebida, então a nova mensagem não será processada (enquanto, ao mesmo tempo, ela e orig_message.msg_id serão reconhecidos). O valor de orig_message.msg_id deve ser menor que o msg_id do contêiner.

Vamos até ficar calados sobre o que msgs_state_info novamente, as orelhas do TL inacabado estão para fora (precisávamos de um vetor de bytes, e nos dois bits inferiores havia um enum e nos dois bits superiores havia sinalizadores). A questão é diferente. Alguém entende por que tudo isso está na prática? em um cliente real necessário?.. Com dificuldade, mas pode-se imaginar algum benefício se uma pessoa estiver envolvida na depuração e no modo interativo - pergunte ao servidor o que e como. Mas aqui os pedidos são descritos ida e volta.

Segue-se que cada parte deve não apenas criptografar e enviar mensagens, mas também armazenar dados sobre si mesmo, sobre as respostas a elas, por um período de tempo desconhecido. A documentação não descreve os tempos nem a aplicabilidade prática desses recursos. de modo nenhum. O mais surpreendente é que eles são realmente usados ​​no código dos clientes oficiais! Aparentemente, foi-lhes dito algo que não estava incluído na documentação pública. Entenda a partir do código por que, não é mais tão simples como no caso de TL - não é uma parte (relativamente) isolada logicamente, mas uma peça ligada à arquitetura da aplicação, ou seja, exigirá muito mais tempo para entender o código do aplicativo.

Pings e horários. Filas.

De tudo, se lembrarmos das suposições sobre a arquitetura do servidor (distribuição de solicitações entre backends), segue-se uma coisa bastante triste - apesar de todas as garantias de entrega no TCP (ou os dados são entregues ou você será informado sobre a lacuna, mas os dados serão entregues antes que o problema ocorra), que as confirmações no próprio MTProto - sem garantias. O servidor pode facilmente perder ou descartar sua mensagem e nada pode ser feito a respeito, apenas use diferentes tipos de muletas.

E antes de tudo, filas de mensagens. Bem, com uma coisa tudo ficou óbvio desde o início - uma mensagem não confirmada deve ser armazenada e reenviada. E depois de que horas? E o bobo da corte o conhece. Talvez aquelas mensagens de serviço viciadas de alguma forma resolvam esse problema com muletas, digamos, no Telegram Desktop existem cerca de 4 filas correspondentes a elas (talvez mais, como já mencionado, para isso você precisa se aprofundar mais em seu código e arquitetura; ao mesmo tempo tempo, sabemos que não pode ser tomado como amostra; um certo número de tipos do esquema MTProto não é usado nele).

Por que isso está acontecendo? Provavelmente, os programadores do servidor não conseguiram garantir a confiabilidade dentro do cluster, ou mesmo o buffer no balanceador frontal, e transferiram esse problema para o cliente. Desesperado, Vasily tentou implementar uma opção alternativa, com apenas duas filas, usando algoritmos do TCP - medindo o RTT para o servidor e ajustando o tamanho da “janela” (nas mensagens) dependendo do número de solicitações não confirmadas. Ou seja, uma heurística tão grosseira para avaliar a carga do servidor é quantas de nossas solicitações ele pode mastigar ao mesmo tempo e não perder.

Bem, isto é, você entende, certo? Se você tiver que implementar o TCP novamente sobre um protocolo executado sobre TCP, isso indica um protocolo muito mal projetado.

Ah, sim, por que você precisa de mais de uma fila e o que isso significa para uma pessoa que trabalha com uma API de alto nível? Olha, você faz uma solicitação, serializa, mas muitas vezes não consegue enviar imediatamente. Por que? Porque a resposta será msg_id, que é temporárioаEu sou um rótulo, cuja atribuição é melhor adiada para o mais tarde possível - caso o servidor a rejeite devido a uma incompatibilidade de tempo entre nós e ele (claro, podemos fazer uma muleta que mude nosso tempo do presente ao servidor adicionando um delta calculado a partir das respostas do servidor - os clientes oficiais fazem isso, mas é grosseiro e impreciso devido ao buffer). Portanto, ao fazer uma requisição com chamada de função local da biblioteca, a mensagem passa pelas seguintes etapas:

  1. Ele está em uma fila e aguarda criptografia.
  2. Nomeado msg_id e a mensagem foi para outra fila - possível encaminhamento; enviar para o soquete.
  3. a) O servidor respondeu MsgsAck - a mensagem foi entregue, excluímos da “outra fila”.
    b) Ou vice-versa, ele não gostou de alguma coisa, respondeu badmsg - reenviar de “outra fila”
    c) Nada se sabe, a mensagem precisa ser reenviada de outra fila – mas não se sabe exatamente quando.
  4. O servidor finalmente respondeu RpcResult - a resposta real (ou erro) - não apenas entregue, mas também processada.

Talvez, o uso de contêineres poderia resolver parcialmente o problema. É quando um monte de mensagens são agrupadas em uma e o servidor responde com uma confirmação a todas elas de uma vez, em um msg_id. Mas ele também rejeitará este pacote, se algo der errado, na sua totalidade.

E neste ponto entram em jogo considerações não técnicas. Pela experiência, vimos muitas muletas e, além disso, veremos agora mais exemplos de maus conselhos e arquitetura - nessas condições, vale a pena confiar e tomar tais decisões? A pergunta é retórica (claro que não).

Sobre o que estamos conversando? Se no tópico “mensagens de drogas sobre mensagens” você ainda pode especular com objeções como “você é estúpido, não entendeu nosso plano brilhante!” (então escreva a documentação primeiro, como pessoas normais deveriam, com justificativas e exemplos de troca de pacotes, depois conversaremos), então os tempos/tempos limite são uma questão puramente prática e específica, tudo aqui é conhecido há muito tempo. O que a documentação nos diz sobre tempos limite?

Um servidor geralmente confirma o recebimento de uma mensagem de um cliente (normalmente, uma consulta RPC) usando uma resposta RPC. Se a resposta demorar muito para chegar, um servidor poderá primeiro enviar uma confirmação de recebimento e, um pouco mais tarde, a própria resposta RPC.

Um cliente normalmente confirma o recebimento de uma mensagem de um servidor (normalmente, uma resposta RPC) adicionando uma confirmação à próxima consulta RPC se ela não for transmitida muito tarde (se for gerada, digamos, 60-120 segundos após o recebimento de uma mensagem do servidor). Entretanto, se por um longo período de tempo não houver razão para enviar mensagens ao servidor ou se houver um grande número de mensagens não confirmadas do servidor (digamos, mais de 16), o cliente transmite uma confirmação independente.

... eu traduzo: nós mesmos não sabemos o quanto e como precisamos disso, então vamos supor que seja assim.

E sobre pings:

Mensagens de ping (PING/PONG)

ping#7abe77ec ping_id:long = Pong;

Uma resposta geralmente é retornada para a mesma conexão:

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

Essas mensagens não requerem confirmações. Um pong é transmitido apenas em resposta a um ping, enquanto um ping pode ser iniciado por qualquer um dos lados.

Fechamento de conexão diferido + PING

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

Funciona como ping. Além disso, após o recebimento, o servidor inicia um cronômetro que fechará a conexão atual shutdown_delay segundos depois, a menos que receba uma nova mensagem do mesmo tipo que redefina automaticamente todos os cronômetros anteriores. Se o cliente enviar esses pings uma vez a cada 60 segundos, por exemplo, ele poderá definir shutdown_delay igual a 75 segundos.

Você está louco?! Em 60 segundos, o trem entrará na estação, deixará e pegará passageiros e novamente perderá contato no túnel. Em 120 segundos, enquanto você ouve, chegará a outro e a conexão provavelmente será interrompida. Bem, está claro de onde vêm as pernas - “Ouvi um toque, mas não sei onde está”, existe o algoritmo de Nagl e a opção TCP_NODELAY, destinada ao trabalho interativo. Mas, com licença, mantenha o valor padrão - 200 Milisegundos Se você realmente deseja representar algo semelhante e economizar em alguns pacotes possíveis, adie-o por 5 segundos ou qualquer que seja o tempo limite da mensagem “O usuário está digitando...” agora. Mas não mais.

E finalmente, pings. Ou seja, verificando a atividade da conexão TCP. É engraçado, mas há cerca de 10 anos escrevi um texto crítico sobre o mensageiro do dormitório do nosso corpo docente - os autores de lá também fizeram ping no servidor do cliente, e não vice-versa. Mas aluno do 3º ano é uma coisa e escritório internacional é outra, né?..

Primeiro, um pequeno programa educacional. Uma conexão TCP, na ausência de troca de pacotes, pode durar semanas. Isso é bom e ruim, dependendo do propósito. É bom se você tivesse uma conexão SSH aberta com o servidor, você saiu do computador, reiniciou o roteador, voltou para o seu lugar - a sessão através deste servidor não foi interrompida (você não digitou nada, não havia pacotes) , é conveniente. É ruim se houver milhares de clientes no servidor, cada um consumindo recursos (olá, Postgres!), e o host do cliente pode ter reiniciado há muito tempo - mas não saberemos disso.

Os sistemas de bate-papo/IM se enquadram no segundo caso por um motivo adicional: status online. Se o usuário “caiu”, é necessário informar seus interlocutores sobre isso. Caso contrário, você vai acabar com um erro que os criadores do Jabber cometeram (e corrigiram por 20 anos) - o usuário se desconectou, mas continuam escrevendo mensagens para ele, acreditando que ele está online (que também ficaram completamente perdidos nestes alguns minutos antes da desconexão ser descoberta). Não, a opção TCP_KEEPALIVE, que muitas pessoas que não entendem como os temporizadores TCP funcionam, lançam aleatoriamente (definindo valores selvagens como dezenas de segundos), não ajudará aqui - você precisa ter certeza de que não apenas o kernel do sistema operacional A parte da máquina do usuário está viva, mas também funcionando normalmente, capaz de responder, e o próprio aplicativo (você acha que ele não pode congelar? O Telegram Desktop no Ubuntu 18.04 congelou para mim mais de uma vez).

É por isso que você tem que pingar servidor cliente, e não vice-versa - se o cliente fizer isso, se a conexão for interrompida, o ping não será entregue, o objetivo não será alcançado.

O que vemos no Telegram? É exatamente o oposto! Bem, isso é. Formalmente, é claro, ambos os lados podem fazer ping um ao outro. Na prática, os clientes usam muleta ping_delay_disconnect, que define o cronômetro no servidor. Bom, com licença, não cabe ao cliente decidir quanto tempo quer morar lá sem ping. O servidor, com base na sua carga, sabe melhor. Mas, claro, se você não se importa com os recursos, então você será o seu próprio Pinóquio malvado, e uma muleta servirá...

Como deveria ter sido projetado?

Acredito que os factos acima referidos indicam claramente que a equipa do Telegram/VKontakte não é muito competente na área dos transportes (e inferior) nível de redes informáticas e as suas baixas qualificações em assuntos relevantes.

Por que tudo acabou sendo tão complicado e como os arquitetos do Telegram podem tentar contestar? O fato de terem tentado fazer uma sessão que sobrevivesse a quebras de conexão TCP, ou seja, o que não foi entregue agora, entregaremos mais tarde. Provavelmente também tentaram fazer um transporte UDP, mas encontraram dificuldades e abandonaram (por isso a documentação está vazia - não havia do que se gabar). Mas devido à falta de compreensão de como as redes em geral e o TCP em particular funcionam, onde você pode confiar neles e onde você precisa fazer isso sozinho (e como), e uma tentativa de combinar isso com a criptografia “dois coelhos com uma pedra”, este é o resultado.

Como foi necessário? Com base no fato de que msg_id é um carimbo de data/hora necessário do ponto de vista criptográfico para evitar ataques de repetição, é um erro anexar uma função de identificador exclusivo a ele. Portanto, sem alterar fundamentalmente a arquitetura atual (quando o fluxo de atualizações é gerado, esse é um tópico de API de alto nível para outra parte desta série de postagens), seria necessário:

  1. O servidor que mantém a conexão TCP com o cliente assume a responsabilidade - se ele leu o soquete, reconheça, processe ou retorne um erro, sem perda. Então a confirmação não é um vetor de ids, mas simplesmente “o último seq_no recebido” - apenas um número, como no TCP (dois números - seu seq e o confirmado). Estamos sempre dentro da sessão, não é?
  2. O carimbo de data e hora para evitar ataques de repetição torna-se um campo separado, à la nonce. Está marcado, mas não afeta mais nada. Chega e uint32 - se nosso sal mudar pelo menos a cada meio dia, podemos alocar 16 bits para os bits de ordem inferior da parte inteira da hora atual, o restante - para uma parte fracionária de segundo (como agora).
  3. Removido msg_id em absoluto - do ponto de vista de distinguir solicitações nos backends, existe, em primeiro lugar, o id do cliente e, em segundo lugar, o id da sessão, concatená-los. Conseqüentemente, apenas uma coisa é suficiente como identificador de solicitação seq_no.

Essa também não é a opção mais bem-sucedida; um aleatório completo poderia servir como identificador - isso já é feito na API de alto nível ao enviar uma mensagem, aliás. Seria melhor refazer completamente a arquitetura de relativa para absoluta, mas isso é assunto para outra parte, não para este post.

API?

Ta-daam! Assim, tendo lutado por um caminho cheio de dores e muletas, finalmente conseguimos enviar quaisquer solicitações ao servidor e receber quaisquer respostas a elas, bem como receber atualizações do servidor (não em resposta a uma solicitação, mas ela mesma nos envia, tipo PUSH, se alguém ficar mais claro assim).

Atenção, agora haverá o único exemplo em Perl no artigo! (para quem não está familiarizado com a sintaxe, o primeiro argumento de Blessed é a estrutura de dados do objeto, o segundo é sua classe):

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

Sim, não é um spoiler de propósito – se você ainda não leu, vá em frente e leia!

Oh, wai~~... como é isso? Algo muito familiar... talvez esta seja a estrutura de dados de uma API Web típica em JSON, exceto que as classes também são anexadas a objetos?..

Então é assim que acontece... Do que se trata, camaradas?.. Tanto esforço - e paramos para descansar onde os programadores Web Apenas começando?..Não seria apenas JSON sobre HTTPS mais simples?! O que ganhamos em troca? O esforço valeu a pena?

Vamos avaliar o que o TL+MTProto nos deu e quais alternativas são possíveis. Bem, o HTTP, que se concentra no modelo de solicitação-resposta, não é adequado, mas pelo menos algo além do TLS?

Serialização compacta. Vendo essa estrutura de dados, semelhante ao JSON, lembro que existem versões binárias dela. Vamos marcar o MsgPack como insuficientemente extensível, mas existe, por exemplo, CBOR - aliás, um padrão descrito em RFC 7049. É notável pelo fato de definir tags, como mecanismo de expansão, e entre já padronizado tem:

  • 25 + 256 - substituindo linhas repetidas por uma referência ao número da linha, um método de compactação barato
  • 26 - objeto Perl serializado com nome de classe e argumentos de construtor
  • 27 - objeto serializado independente de linguagem com nome de tipo e argumentos de construtor

Bem, tentei serializar os mesmos dados em TL e em CBOR com string e empacotamento de objetos habilitados. O resultado começou a variar em favor do CBOR em torno de um megabyte:

cborlen=1039673 tl_len=1095092

Assim, saída: Existem formatos substancialmente mais simples que não estão sujeitos ao problema de falha de sincronização ou identificador desconhecido, com eficiência comparável.

Estabelecimento de conexão rápida. Isso significa zero RTT após a reconexão (quando a chave já foi gerada uma vez) - aplicável desde a primeira mensagem MTProto, mas com algumas ressalvas - atingiu o mesmo sal, a sessão não está podre, etc. O que o TLS nos oferece? Citação no tópico:

Ao usar PFS em TLS, tickets de sessão TLS (RFC 5077) para retomar uma sessão criptografada sem renegociar chaves e sem armazenar informações de chave no servidor. Ao abrir a primeira conexão e criar chaves, o servidor criptografa o estado da conexão e o transmite ao cliente (na forma de um ticket de sessão). Assim, quando a conexão é retomada, o cliente envia um ticket de sessão, incluindo a chave da sessão, de volta ao servidor. O ticket em si é criptografado com uma chave temporária (chave de ticket de sessão), que é armazenada no servidor e deve ser distribuída entre todos os servidores frontend que processam SSL em soluções clusterizadas.[10]. Assim, a introdução de um ticket de sessão pode violar o PFS se as chaves temporárias do servidor forem comprometidas, por exemplo, quando são armazenadas por um longo período (OpenSSL, nginx, Apache as armazenam por padrão durante toda a duração do programa; sites populares usam a chave por várias horas, até dias).

Aqui o RTT não é zero, você precisa trocar pelo menos ClientHello e ServerHello, após o qual o cliente pode enviar dados junto com Finished. Mas aqui devemos lembrar que não temos a Web, com seu monte de conexões recém-abertas, mas um mensageiro, cuja conexão é muitas vezes uma solicitação relativamente curta e de vida mais ou menos longa para páginas da Web - tudo é multiplexado internamente. Ou seja, é bastante aceitável se não nos depararmos com um trecho de metrô muito ruim.

Esqueceu mais alguma coisa? Escreva nos comentários.

Continua!

Na segunda parte desta série de posts consideraremos questões não técnicas, mas organizacionais - abordagens, ideologia, interface, atitude em relação aos usuários, etc. Com base, porém, nas informações técnicas aqui apresentadas.

A terceira parte continuará a analisar a componente técnica/experiência de desenvolvimento. Você aprenderá, em particular:

  • continuação do pandemônio com a variedade de tipos de TL
  • coisas desconhecidas sobre canais e supergrupos
  • por que os diálogos são piores que a lista
  • sobre endereçamento de mensagens absoluto versus relativo
  • qual é a diferença entre foto e imagem
  • como os emojis interferem no texto em itálico

e outras muletas! Fique atento!

Fonte: habr.com

Adicionar um comentário