Crítica ao protocolo e aos enfoques organizativos de Telegram. Parte 1, técnica: experiencia de escribir un cliente desde cero - TL, MT

Recentemente, as publicacións sobre o bo que é Telegram, o brillante e experimentado que teñen os irmáns Durov na construción de sistemas de rede, etc., comezaron a aparecer con máis frecuencia en Habré. Ao mesmo tempo, moi poucas persoas se mergullaron realmente no dispositivo técnico; como moito, usan unha API de Bot baseada en JSON bastante sinxela (e bastante diferente de MTProto) e normalmente só aceptan sobre a fe todos os eloxios e PR que xiran arredor do mensaxeiro. Hai case ano e medio, o meu compañeiro da ONG Eshelon Vasily (lamentablemente, a súa conta sobre Habré foi borrada xunto co borrador) comezou a escribir o seu propio cliente de Telegram desde cero en Perl, e máis tarde uniuse o autor destas liñas. Por que Perl, preguntarán algúns inmediatamente? Porque este tipo de proxectos xa existen noutros idiomas, de feito, non é este o caso, podería haber calquera outro idioma onde non exista biblioteca preparada, e en consecuencia o autor debe ir ata o final dende cero. Ademais, a criptografía é unha cuestión de confianza, pero verificar. Cun produto dirixido á seguridade, non pode simplemente confiar nunha biblioteca preparada do fabricante e confiar cegamente nela (non obstante, este é un tema para a segunda parte). Polo momento, a biblioteca funciona bastante ben a nivel "medio" (permíteche facer calquera solicitude da API).

Non obstante, non haberá moita criptografía ou matemática nesta serie de publicacións. Pero haberá moitos outros detalles técnicos e muletas arquitectónicas (tamén útiles para os que non escribirán desde cero, pero usarán a biblioteca en calquera idioma). Entón, o obxectivo principal era intentar implementar o cliente desde cero segundo a documentación oficial. É dicir, supoñamos que o código fonte dos clientes oficiais está pechado (de novo, na segunda parte trataremos con máis detalle o tema do feito de que isto é certo pasa así), pero, como nos vellos tempos, por exemplo, hai un estándar como RFC: é posible escribir un cliente só segundo a especificación, "sen mirar" o código fonte, xa sexa oficial (Telegram Desktop, móbil), ou Teletón non oficial?

Imaxe:

Documentación... existe, non? É verdade?..

Fragmentos de notas para este artigo comezaron a recollerse o verán pasado. Todo este tempo na páxina web oficial https://core.telegram.org A documentación era a partir da capa 23, é dicir. pegado nalgún lugar en 2014 (lembra, non había nin canles daquela?). Por suposto, en teoría, isto debería permitirnos implementar un cliente con funcionalidade nese momento en 2014. Pero mesmo neste estado, a documentación estaba, en primeiro lugar, incompleta e, en segundo lugar, nalgúns lugares se contradí. Hai pouco máis dun mes, en setembro de 2019, así foi por casualidade Descubriuse que houbo unha gran actualización da documentación do sitio, para a completamente recente Capa 105, cunha nota de que agora hai que ler todo de novo. De feito, moitos artigos foron revisados, pero moitos permaneceron sen cambios. Polo tanto, ao ler as críticas a continuación sobre a documentación, debes ter en conta que algunhas destas cousas xa non son relevantes, pero algunhas aínda o son bastante. Despois de todo, 5 anos no mundo moderno non é só moito tempo, senón moi Moito. Desde aqueles tempos (especialmente se non se teñen en conta os sitios de xeochat descartados e revividos desde entón), o número de métodos API do esquema pasou de cen a máis de douscentos cincuenta.

Por onde comezar como autor novo?

Non importa se escribes desde cero ou utilizas, por exemplo, bibliotecas preparadas como Telethon para Python ou Madeline para PHP, en calquera caso, necesitarás primeiro rexistrar a súa solicitude - obter parámetros api_id и api_hash (os que traballaron coa API VKontakte entenden inmediatamente) polo cal o servidor identificará a aplicación. Isto ten que faino por razóns legais, pero falaremos máis sobre por que os autores das bibliotecas non poden publicalo na segunda parte. Podes estar satisfeito cos valores da proba, aínda que son moi limitados; o certo é que agora podes rexistrarte só un aplicación, así que non te apures de cabeza.

Agora, dende o punto de vista técnico, debería interesarnos que despois do rexistro recibamos notificacións de Telegram sobre actualizacións de documentación, protocolo, etc. É dicir, poderíase asumir que o sitio cos peiraos estaba simplemente abandonado e continuou traballando especificamente cos que comezaron a facer clientes, porque é máis doado. Pero non, non se observou nada parecido, non chegou ningunha información.

E se escribes desde cero, usar os parámetros obtidos aínda está moi lonxe. Aínda que https://core.telegram.org/ e fala deles en Getting Started en primeiro lugar, de feito, primeiro terás que implementar Protocolo MTProto - pero se creses disposición segundo o modelo OSI ao final da páxina para unha descrición xeral do protocolo, entón é completamente en balde.

De feito, tanto antes como despois de MTProto, en varios niveis á vez (como din os rededores estranxeiros que traballan no núcleo do sistema operativo, a violación da capa), un tema grande, doloroso e terrible estorbarase...

Serialización binaria: TL (Type Language) e o seu esquema, capas e moitas outras palabras de medo

Este tema, de feito, é a clave dos problemas de Telegram. E haberá moitas palabras terribles se intentas afondar nela.

Entón, aquí está o diagrama. Se che ocorre esta palabra, di: Esquema JSON, Pensaches correctamente. O obxectivo é o mesmo: algunha linguaxe para describir un posible conxunto de datos transmitidos. Aquí rematan as semellanzas. Se dende a páxina Protocolo MTProto, ou dende a árbore de orixe do cliente oficial, tentaremos abrir algún esquema, veremos algo así 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;

Unha persoa que ve isto por primeira vez poderá recoñecer intuitivamente só parte do que está escrito; ben, aparentemente son estruturas (aínda que onde está o nome, á esquerda ou á dereita?), hai campos nelas, despois de que un tipo segue despois dos dous puntos... probablemente. Aquí entre corchetes angulares probablemente haxa modelos como en C++ (de feito, non completamente). E que significan todos os outros símbolos, signos de interrogación, signos de exclamación, porcentaxes, signos hash (e obviamente significan cousas diferentes en diferentes lugares), ás veces presentes e ás veces non, números hexadecimais e, o máis importante, como obter isto правильный (que non será rexeitado polo servidor) fluxo de bytes? Terás que ler a documentación (si, hai ligazóns ao esquema na versión JSON preto, pero iso non o deixa máis claro).

Abre a páxina Serialización de datos binarios e mergúllase no mundo máxico dos cogomelos e as matemáticas discretas, algo semellante ao matan de 4o curso. Alfabeto, tipo, valor, combinador, combinador funcional, forma normal, tipo composto, tipo polimórfico... e iso é só a primeira páxina! O seguinte agarda por ti Linguaxe TL, que, aínda que xa contén un exemplo de solicitude e resposta trivial, non dá resposta en absoluto a casos máis típicos, o que significa que terás que percorrer un recuento de matemáticas traducidos do ruso ao inglés noutros oito incrustados. páxinas!

Os lectores familiarizados coas linguaxes funcionais e as inferencias automáticas verán, por suposto, a linguaxe de descrición nesta lingua, mesmo a partir do exemplo, como moito máis familiar, e poden dicir que en principio non é nada malo. As obxeccións a isto son:

  • si, o obxectivo soa ben, pero por desgraza, ela non conseguido
  • A educación nas universidades rusas varía incluso entre as especialidades de TI - non todos tomaron o curso correspondente
  • Finalmente, como veremos, na práctica é non se require, xa que só se usa un subconxunto limitado incluso do TL que se describiu

Como se dixo LeonNerd na canle #perl na rede FreeNode IRC, que intentou implementar unha porta de Telegram a Matrix (a tradución da cita é inexacta da memoria):

Parece que alguén se introduciu na teoría de tipos por primeira vez, emocionouse e comezou a tentar xogar con ela, sen importarlle realmente se fose necesario na práctica.

Comprobe por si mesmo, se a necesidade de bare-types (int, long, etc.) como algo elemental non suscita dúbidas -en última instancia, deben implementarse manualmente-, por exemplo, intentemos derivar deles. vector. É dicir, de feito, matriz, se chamas ás cousas resultantes polos seus nomes propios.

Pero antes

Unha breve descrición dun subconxunto de sintaxe TL para aqueles que non lean a documentación 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 definición sempre comeza construtor, despois de que opcionalmente (na práctica - sempre) a través do símbolo # debe ser CRC32 a partir da cadea de descrición normalizada deste tipo. A continuación vén unha descrición dos campos; se os existen, o tipo pode estar baleiro. Todo isto remata cun signo igual, o nome do tipo ao que pertence este construtor, é dicir, de feito, o subtipo. O tipo á dereita do signo de igual é polimórfico - é dicir, poden corresponderlle varios tipos específicos.

Se a definición ocorre despois da liña ---functions---, entón a sintaxe seguirá sendo a mesma, pero o significado será diferente: o construtor converterase no nome da función RPC, os campos converteranse en parámetros (ben, é dicir, seguirá sendo exactamente a mesma estrutura dada, como se describe a continuación , este será simplemente o significado asignado), e o "tipo polimórfico" - o tipo do resultado devolto. É certo, aínda permanecerá polimórfico, só se define na sección ---types---, pero este construtor "non será considerado". Sobrecargar os tipos de funcións chamadas polos seus argumentos, é dicir. Por algunha razón, varias funcións co mesmo nome pero sinaturas diferentes, como en C++, non están previstas no TL.

Por que "construtor" e "polimórfico" se non é POO? Ben, de feito, será máis fácil que alguén pense nisto en termos de POO: un tipo polimórfico como clase abstracta, e os construtores son as súas clases descendentes directas, e final na terminoloxía de varias linguas. De feito, por suposto, só aquí semellanza con métodos construtores sobrecargados reais en linguaxes de programación OO. Dado que aquí só hai estruturas de datos, non hai métodos (aínda que a descrición de funcións e métodos máis adiante é bastante capaz de crear confusión na cabeza de que existen, pero iso é unha cuestión diferente) - pódese pensar nun construtor como un valor de que estase a construír escriba ao ler un fluxo de bytes.

Como ocorre isto? O deserializador, que sempre le 4 bytes, ve o valor 0xcrc32 - e comprende o que vai pasar despois field1 con tipo int, é dicir. le exactamente 4 bytes, neste o campo superior co tipo PolymorType ler. Vese 0x2crc32 e entende que hai dous campos máis aló, primeiro long, o que significa que lemos 8 bytes. E de novo un tipo complexo, que se deserializa do mesmo xeito. Por exemplo, Type3 poderían ser declarados no circuíto en canto dous construtores, respectivamente, deben reunirse calquera 0x12abcd34, despois de que cómpre ler 4 bytes máis intOu 0x6789cdef, despois do cal non haberá nada. Calquera outra cousa: debes lanzar unha excepción. De todos os xeitos, despois disto volvemos a ler 4 bytes int campos field_c в constructorTwo e con iso rematamos de ler o noso PolymorType.

En fin, se te pillan 0xdeadcrc para constructorThree, entón todo faise máis complicado. O noso primeiro campo é bit_flags_of_what_really_present con tipo # - de feito, isto é só un alias para o tipo nat, que significa "número natural". É dicir, de feito, int unsigned é, por certo, o único caso en que os números sen signo ocorren en circuítos reais. Entón, a continuación é unha construción cun signo de interrogación, o que significa que este campo estará presente no cable só se o bit correspondente está definido no campo referido (aproximadamente como un operador ternario). Entón, supoñamos que este bit foi definido, o que significa que aínda necesitamos ler un campo como Type, que no noso exemplo ten 2 construtores. Un está baleiro (está composto só polo identificador), o outro ten un campo ids con tipo ids:Vector<long>.

Podes pensar que tanto os modelos como os xenéricos están nos profesionais ou en Java. Pero non. Case. Isto o único caso de usar corchetes angulares en circuítos reais, e úsase SÓ para Vector. Nun fluxo de bytes, estes serán 4 CRC32 bytes para o tipo Vector en si, sempre o mesmo, despois 4 bytes - o número de elementos da matriz, e despois estes elementos.

Engádese a isto o feito de que a serialización sempre ocorre en palabras de 4 bytes, todos os tipos son múltiplos; tamén se describen os tipos incorporados. bytes и string con serialización manual da lonxitude e este aliñamento por 4 - ben, parece que parece normal e incluso relativamente eficaz? Aínda que se afirma que TL é unha serialización binaria efectiva, ao carallo, coa expansión de case calquera cousa, incluso os valores booleanos e as cadeas dun só carácter a 4 bytes, JSON aínda será moito máis groso? Mira, ata os campos innecesarios pódense omitir con marcas de bits, todo é bastante bo e incluso extensible para o futuro, entón por que non engadir novos campos opcionais ao construtor máis tarde?

Pero non, se le non a miña breve descrición, senón a documentación completa e pensa na implementación. En primeiro lugar, o CRC32 do construtor calcúlase segundo a liña normalizada da descrición de texto do esquema (eliminar espazos en branco extra, etc.), polo que se se engade un novo campo, a liña de descrición do tipo cambiará e, polo tanto, o seu CRC32 e , en consecuencia, serialización. E que faría o vello cliente se recibise un campo con bandeiras novas e non sabe que facer con elas a continuación?...

En segundo lugar, lembremos CRC32, que se usa aquí esencialmente como funcións hash para determinar de forma única que tipo está a ser (des)serializado. Aquí estamos ante o problema das colisións, e non, a probabilidade non é unha en 232, senón moito maior. Quen recordou que CRC32 está deseñado para detectar (e corrixir) erros na canle de comunicación e, en consecuencia, mellora estas propiedades en detrimento doutras? Por exemplo, non lle importa reorganizar os bytes: se calculas CRC32 a partir de dúas liñas, na segunda intercambias os primeiros 4 bytes cos seguintes 4 bytes - será o mesmo. Cando a nosa entrada son cadeas de texto do alfabeto latino (e un pouco de puntuación), e estes nomes non son especialmente aleatorios, a probabilidade de tal reordenación aumenta moito.

Por certo, quen comprobou o que había? realmente CRC32? Un dos primeiros códigos fonte (mesmo antes de Waltman) tiña unha función hash que multiplicaba cada carácter polo número 239, tan querido por esta xente, ha ha!

Finalmente, está ben, decatámonos de que os construtores cun tipo de campo Vector<int> и Vector<PolymorType> terá CRC32 diferente. E o rendemento en liña? E dende o punto de vista teórico, isto pasa a formar parte do tipo? Digamos que pasamos unha matriz de dez mil números, ben con Vector<int> todo está claro, a lonxitude e outros 40000 bytes. E se isto Vector<Type2>, que consta dun só campo int e está só no tipo: necesitamos repetir 10000xabcdef0 34 veces e despois 4 bytes int, ou a linguaxe é capaz de INDEPENDELA por nós do construtor fixedVec e en lugar de 80000 bytes, transferir de novo só 40000?

Esta non é unha pregunta teórica ociosa - imaxina que recibes unha lista de usuarios do grupo, cada un dos cales ten unha identificación, nome e apelidos - a diferenza na cantidade de datos transferidos a través dunha conexión móbil pode ser significativa. É precisamente a eficacia da serialización de Telegram o que se nos anuncia.

Entón ...

Vector, que nunca foi lanzado

Se tentas percorrer as páxinas de descrición de combinadores e así por diante, verás que un vector (e incluso unha matriz) está a tentar formalmente saír a través de tuplas de varias follas. Pero ao final esquécense, sáltase o paso final e simplemente dáse unha definición dun vector, que aínda non está ligado a un tipo. Que pasa? En linguas programación, especialmente os funcionais, é bastante típico describir a estrutura de forma recursiva - o compilador coa súa avaliación preguiceiro entenderá e fará todo por si mesmo. Na lingua serialización de datos o que se necesita é EFICIENCIA: abonda con describir simplemente список, é dicir. estrutura de dous elementos: o primeiro é un elemento de datos, o segundo é a mesma estrutura ou un espazo baleiro para a cola (paquete (cons) en Lisp). Pero isto, obviamente, requirirá de cada un elemento gasta 4 bytes adicionais (CRC32 no caso de TL) para describir o seu tipo. Unha matriz tamén se pode describir facilmente tamaño fixo, pero no caso dunha matriz de lonxitude descoñecida de antemán, interrompémonos.

Polo tanto, dado que TL non permite a saída dun vector, tivo que engadirse ao lado. En definitiva, a documentación di:

A serialización usa sempre o mesmo construtor "vector" (const 0x1cb5c415 = crc32 ("vector t:Type # [ t ] = Vector t") que non depende do valor específico da variable de tipo t.

O valor do parámetro opcional t non está implicado na serialización xa que se deriva do tipo de resultado (coñecido sempre antes da deserialización).

Bótalle unha ollada máis atentamente: vector {t:Type} # [ t ] = Vector t - pero nada Esta definición en si non di que o primeiro número debe ser igual á lonxitude do vector! E non vén de ningures. Este é un dato que hai que ter en conta e implementar coas túas mans. Noutro lugar, a documentación incluso menciona honestamente que o tipo non é real:

O pseudotipo polimórfico Vector t é un "tipo" cuxo valor é unha secuencia de valores de calquera tipo t, xa sexa en caixa ou espido.

... pero non se centra niso. Cando ti, cansado de andar polo estiramento das matemáticas (quizais incluso coñezas desde un curso universitario), decides renunciar e realmente miras como traballalas na práctica, a impresión que deixas na túa cabeza é que isto é serio. Matemáticas no núcleo, foi claramente inventado por Cool People (dous matemáticos - gañador ACM), e non calquera. O obxectivo - presumir - conseguiuse.

Por certo, sobre o número. Lembrámosche iso # é un sinónimo nat, número natural:

Hai expresións tipo (tipo-expr) e expresións numéricas (nat-expr). Non obstante, defínense do mesmo xeito.

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

pero na gramática descríbense do mesmo xeito, é dicir. Esta diferenza debe ser recordada de novo e posta en práctica a man.

Ben, si, tipos de modelos (vector<int>, vector<User>) teñen un identificador común (#1cb5c415), é dicir. se sabe que a convocatoria se anuncia como

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

entón xa non estás esperando só por un vector, senón por un vector de usuarios. Máis precisamente, debería espera -en código real, cada elemento, se non é un tipo puro, terá un construtor, e en boa forma na implementación sería necesario verificalo - pero enviámonos exactamente en todos os elementos deste vector ese tipo? E se fose algún tipo de PHP, no que unha matriz pode conter diferentes tipos en diferentes elementos?

Neste punto comezas a pensar: é necesario tal TL? Quizais para o carro sería posible utilizar un serializador humano, o mesmo protobuf que xa existía entón? Esa era a teoría, vexamos a práctica.

Implementacións de TL existentes en código

TL naceu nas profundidades de VKontakte mesmo antes dos famosos eventos coa venda da participación de Durov e (seguramente), mesmo antes de que comezase o desenvolvemento de Telegram. E en código aberto código fonte da primeira implementación podes atopar moitas muletas divertidas. E a linguaxe en si implementouse alí de forma máis completa que agora en Telegram. Por exemplo, os hash non se usan en absoluto no esquema (é dicir, un pseudotipo incorporado (como un vector) con comportamento desviado). Ou

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

pero consideremos, en aras da integridade, rastrexar, por así dicilo, a evolución do Xigante do Pensamento.

#define ZHUKOV_BYTES_HACK

#ifdef ZHUKOV_BYTES_HACK

/* dirty hack for Zhukov request */

Ou esta fermosa:

    static const char *reserved_words_polymorhic[] = {

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

      };

Este fragmento trata sobre modelos como:

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

Esta é a definición dun tipo de modelo de mapa hash como vector de pares int - Type. En C++ sería algo así:

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

así, alpha - palabra clave! Pero só en C++ podes escribir T, pero deberías escribir alfa, beta... Pero non máis de 8 parámetros, aí remata a fantasía. Parece que noutrora en San Petersburgo tiveron lugar algúns diálogos coma este:

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

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

Pero isto foi sobre a primeira implementación publicada de TL "en xeral". Pasemos a considerar as implementacións nos propios clientes de Telegram.

Palabra a Vasily:

Vasily, [09.10.18 17:07] Sobre todo, o cu está quente porque crearon unha morea de abstraccións, e despois marteláronas cun parafuso e cubriron o xerador de código con muletas.
Como resultado, primeiro desde dock pilot.jpg
A continuación, dende o código dzhekichan.webp

Por suposto, das persoas familiarizadas cos algoritmos e as matemáticas, podemos esperar que leran a Aho, Ullmann e estean familiarizados coas ferramentas que se converteron en estándar de facto na industria ao longo das décadas para escribir os seus compiladores DSL, non?

Por telegrama-cli é Vitaly Valtman, como se pode entender pola aparición do formato TLO fóra dos seus límites (cli), un membro do equipo; agora asignouse unha biblioteca para a análise de TL separado, cal é a impresión dela Analizador TL? ..

16.12 04:18 Vasily: Creo que alguén non dominaba lex+yacc
16.12 04:18 Vasily: Non podo explicalo doutro xeito
16.12 04:18 Vasily: ben, ou pagáronlles polo número de liñas en VK
16.12 04:19 Vasily: 3k+ liñas etc.<censored> en lugar dun analizador

Quizais unha excepción? A ver como fai 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);

Máis de 1100 liñas en Python, un par de expresións regulares + casos especiais como un vector, que, por suposto, está declarado no esquema como debería ser segundo a sintaxe TL, pero confiaron nesta sintaxe para analizalo... Xorde a pregunta, por que foi todo un milagre?иÉ máis en capas se ninguén o vai analizar segundo a documentación de todos os xeitos?!

Por certo... Lembras que falamos da comprobación CRC32? Así, no xerador de código de Telegram Desktop hai unha lista de excepcións para aqueles tipos nos que se calcula o CRC32 non coincide coa indicada no diagrama!

Vasily, [18.12/22 49:XNUMX] e aquí pensaría se é necesario tal TL
se quixese meterse con implementacións alternativas, comezaría a inserir saltos de liña, a metade dos analizadores romperanse nas definicións de varias liñas.
tdesktop, con todo, tamén

Lembra o punto sobre unha liña, volveremos sobre el un pouco máis tarde.

Vale, telegram-cli non é oficial, Telegram Desktop é oficial, pero que pasa cos demais? Quen sabe?... No código do cliente de Android non había ningún analizador de esquemas (o que suscita preguntas sobre o código aberto, pero isto é para a segunda parte), pero había varias outras pezas de código divertidas, pero máis sobre elas no subsección a continuación.

Que outras preguntas suscita a serialización na práctica? Por exemplo, fixeron moitas cousas, por suposto, con campos de bits e campos condicionais:

Vasily: flags.0? true
significa que o campo está presente e é igual a verdadeiro se se establece a bandeira

Vasily: flags.1? int
significa que o campo está presente e debe ser deserializado

Vasily: Cu, non te preocupes polo que fas!
Vasily: Hai unha mención nalgún lugar do documento de que o verdadeiro é un tipo simple de lonxitude cero, pero é imposible reunir nada do seu documento.
Vasily: Nas implementacións de código aberto tampouco é o caso, pero hai unha morea de muletas e soportes

E a Teletón? Mirando cara adiante ao tema de MTProto, un exemplo - na documentación hai tales pezas, pero o sinal % descríbese só como "correspondente a un determinado tipo nu", é dicir. nos exemplos seguintes hai un erro ou algo non documentado:

Vasily, [22.06.18 18:38] Nun só lugar:

msg_container#73f1f8dc messages:vector message = MessageContainer;

En diferente:

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

E estas son dúas grandes diferenzas, na vida real aparece algún tipo de vector espido

Non vin unha definición vectorial simple e non atopei ningunha

A análise está escrita a man en teletón

No seu diagrama coméntase a definición msg_container

De novo, a pregunta segue sendo sobre %. Non se describe.

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

Vasily, [22.06.18 19:23] Pero o seu analizador TL en motores normais tampouco o comerá

// parsed manually

TL é unha fermosa abstracción, ninguén a implementa completamente

E % non está na súa versión do esquema

Pero aquí a documentación contradíse a si mesma, así que idk

Atopouse na gramática, simplemente poderían esquecerse de describir a semántica

Viches o documento en TL, non podes descifralo sen medio litro

"Ben, digamos", dirá outro lector, "ti criticas algo, así que móstrame como se debe facer".

Vasily responde: "En canto ao analizador, gústanme cousas 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 ) ); }
            ;

dalgún xeito gústalle mellor 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 TODO:

    ---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. máis sinxelo é dicilo suavemente".

En xeral, como resultado, o analizador e o xerador de código para o subconxunto de TL realmente usado encaixan en aproximadamente 100 liñas de gramática e ~300 liñas do xerador (contando todos printcódigo xerado de '), incluíndo información de tipo para a introspección en cada clase. Cada tipo polimórfico convértese nunha clase base abstracta baleira, e os construtores herdan dela e teñen métodos de serialización e deserialización.

Falta de tipos na linguaxe tipográfica

Escribir forte é bo, non? Non, isto non é un holivar (aínda que prefiro as linguaxes dinámicas), senón un postulado no marco da TL. En función del, a lingua debería proporcionarnos todo tipo de comprobacións. Ben, vale, quizais non el mesmo, senón a implementación, pero debería polo menos describilos. E que tipo de oportunidades queremos?

En primeiro lugar, as limitacións. Aquí vemos na documentación para subir ficheiros:

O contido binario do ficheiro divídese entón en partes. Todas as pezas deben ter o mesmo tamaño ( tamaño_parte ) e deberán cumprirse as seguintes condicións:

  • part_size % 1024 = 0 (divisible por 1 KB)
  • 524288 % part_size = 0 (512 KB deben ser divisibles uniformemente por part_size)

A última parte non ten que cumprir estas condicións, sempre que o seu tamaño sexa inferior a part_size.

Cada parte debe ter un número de secuencia, parte_ficheiro, cun valor que vai de 0 a 2,999.

Despois de particionar o ficheiro, cómpre escoller un método para gardalo no servidor. Use upload.saveBigFilePart no caso de que o tamaño completo do ficheiro sexa superior a 10 MB e upload.saveFilePart para ficheiros máis pequenos.
[…] pódese devolver un dos seguintes erros de entrada de datos:

  • FILE_PARTS_INVALID — Número de pezas non válido. O valor non está entre 1..3000

Está algo disto no diagrama? É isto expresábel dalgún xeito usando TL? Non. Pero desculpe, ata o Turbo Pascal do avó foi capaz de describir os tipos especificados intervalos. E sabía unha cousa máis, agora máis coñecida como enum - un tipo que consiste nunha enumeración dun número fixo (pequeno) de valores. En linguaxes como C - numérico, teña en conta que ata agora só falamos de tipos números. Pero tamén hai matrices, cadeas... por exemplo, estaría ben describir que esta cadea só pode conter un número de teléfono, non?

Nada disto está no TL. Pero hai, por exemplo, no esquema JSON. E se alguén pode discutir sobre a divisibilidade de 512 KB, que aínda debe verificarse no código, asegúrese de que o cliente simplemente non puiden enviar un número fóra do rango 1..3000 (e non puido xurdir o erro correspondente) sería posible, non?...

Por certo, sobre erros e valores de retorno. Mesmo os que traballaron con TL desenfocan os seus ollos; cada un unha función en TL realmente pode devolver non só o tipo de retorno descrito, senón tamén un erro. Pero isto non se pode deducir de ningún xeito usando o propio TL. Por suposto, xa está claro e non hai necesidade de nada na práctica (aínda que, de feito, RPC pódese facer de diferentes xeitos, volveremos sobre isto máis adiante) - pero que dicir da Pureza dos conceptos de Matemáticas de Tipos Abstractos do mundo celestial?.. Eu collín o remolcador - así que coincida.

E para rematar, que pasa coa lexibilidade? Ben, alí, en xeral, gustaríame Description téñao ben no esquema (no esquema JSON, de novo, é), pero se xa estás esforzado con el, entón que pasa co lado práctico, polo menos trivial mirar as diferenzas durante as actualizacións? Comprobe vostede mesmo en 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, pero GitHub, por exemplo, négase a destacar os cambios dentro de liñas tan longas. O xogo "atopa 10 diferenzas", e o que o cerebro ve inmediatamente é que os comezos e finais en ambos os exemplos son iguais, hai que ler tediosamente nalgún lugar do medio... Na miña opinión, isto non é só en teoría, pero puramente visualmente sucio e desleixado.

Por certo, sobre a pureza da teoría. Por que necesitamos campos de bits? Non parece que eles cheiro malo desde o punto de vista da teoría de tipos? A explicación pódese ver en versións anteriores do diagrama. Ao principio, si, así foi, por cada espirro creábase un novo tipo. Estes rudimentos aínda existen 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;

Pero agora imaxina, se tes 5 campos opcionais na túa estrutura, necesitarás 32 tipos para todas as opcións posibles. Explosión combinatoria. Así, a pureza cristalina da teoría TL volveu esnaquizar contra o cu de ferro fundido da dura realidade da serialización.

Ademais, nalgúns lugares estes propios rapaces violan a súa propia tipoloxía. Por exemplo, en MTProto (seguinte capítulo) a resposta pódese comprimir mediante Gzip, todo está ben, excepto que se violan as capas e o circuíto. Unha vez máis, non foi o propio RpcResult o que se recolleu, senón o seu contido. Ben, por que facelo?... Tiven que cortar nunha muleta para que a compresión funcionara en calquera lugar.

Ou outro exemplo, unha vez descubrimos un erro: foi enviado InputPeerUser en vez de InputUser. Ou viceversa. Pero funcionou! É dicir, ao servidor non lle importaba o tipo. Como pode ser isto? A resposta pode darnos fragmentos de código de 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);

Noutras palabras, aquí é onde se realiza a serialización MANUALMENTE, código non xerado! Quizais o servidor se implemente dun xeito similar?... En principio, isto funcionará se se fai unha vez, pero como se pode apoiar máis tarde durante as actualizacións? É por iso que se inventou o esquema? E aquí pasamos á seguinte pregunta.

Versionado. Capas

Por que as versións esquemáticas se chaman capas só se pode especular en función da historia dos esquemas publicados. Ao parecer, nun principio os autores pensaron que se podían facer cousas básicas utilizando o esquema inalterado, e só cando fose necesario, para solicitudes específicas, indicar que se estaban a facer utilizando unha versión diferente. En principio, incluso é unha boa idea, e o novo será, por así decirlo, "mixto", en capas sobre o vello. Pero imos ver como se fixo. É certo, non puiden miralo desde o principio - é divertido, pero o diagrama da capa base simplemente non existe. As capas comezaron con 2. A documentación fálanos dunha característica especial de TL:

Se un cliente admite a capa 2, debe utilizarse o seguinte construtor:

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

Na práctica, isto significa que antes de cada chamada á API, un int co valor 0x289dd1f6 debe engadirse antes do número de método.

Parece normal. Pero que pasou despois? Entón apareceu

invokeWithLayer3#b7475268 query:!X = X;

Entón, que é o seguinte? Como podes adiviñar,

invokeWithLayer4#dea0d430 query:!X = X;

Divertido? Non, é cedo para rir, pensa niso cada unha solicitude doutra capa debe envolverse nun tipo tan especial: se os tes todos diferentes, como podes distinguilos? E engadir só 4 bytes por diante é un método bastante eficiente. Entón,

invokeWithLayer5#417a57ae query:!X = X;

Pero é obvio que despois dun tempo isto converterase nunha especie de bacanal. E chegou a solución:

Actualización: comezando coa capa 9, métodos auxiliares invokeWithLayerN só se pode usar xunto con initConnection

Hurra! Despois de 9 versións, por fin chegamos ao que se facía nos protocolos de Internet nos anos 80: poñernos de acordo sobre a versión unha vez ao comezo da conexión.

Entón, que é o seguinte?...

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

Pero agora aínda podes rir. Só despois doutras 9 capas, finalmente engadiuse un construtor universal cun número de versión, que só debe chamarse unha vez ao comezo da conexión, e o significado das capas parecía que desapareceu, agora é só unha versión condicional, como en todas partes. Problema resolto.

Exactamente?...

Vasily, [16.07.18 14:01] Aínda o venres pensei:
O teleservidor envía eventos sen solicitude. As solicitudes deben envolverse en InvokeWithLayer. O servidor non envolve as actualizacións; non hai estrutura para envolver respostas e actualizacións.

Eses. o cliente non pode especificar a capa na que quere actualizacións

Vadim Goncharov, [16.07.18 14:02] InvokeWithLayer non é unha muleta en principio?

Vasily, [16.07.18 14:02] Este é o único camiño

Vadim Goncharov, [16.07.18 14:02] que esencialmente debería significar acordar a capa ao comezo da sesión

Por certo, despréndese que non se proporciona unha baixada do cliente

Actualizacións, é dicir. tipo Updates no esquema, isto é o que o servidor envía ao cliente non en resposta a unha solicitude da API, senón de forma independente cando se produce un evento. Este é un tema complexo que se comentará noutra publicación, pero de momento é importante saber que o servidor garda Actualizacións aínda que o cliente estea fóra de liña.

Así, se rexeitas a envolver de cada un paquete para indicar a súa versión, isto leva loxicamente aos seguintes posibles problemas:

  • o servidor envía actualizacións ao cliente mesmo antes de que o cliente teña informado que versión admite
  • que debo facer despois de actualizar o cliente?
  • quen garantíasque a opinión do servidor sobre o número de capa non cambiará durante o proceso?

Cres que isto é unha especulación puramente teórica, e na práctica isto non pode ocorrer, porque o servidor está escrito correctamente (polo menos, está ben probado)? Ha! Non importa como sexa!

Isto é exactamente o que nos atopamos en agosto. O 14 de agosto apareceron mensaxes de que se estaba a actualizar algo nos servidores de Telegram... e despois nos rexistros:

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 despois varios megabytes de trazos de pila (ben, ao mesmo tempo arranxouse o rexistro). Despois de todo, se algo non se recoñece no teu TL, é binario por sinatura, máis abaixo na liña TODOS vai, a decodificación será imposible. Que deberías facer nunha situación así?

Ben, o primeiro que se lle ocorre a calquera é desconectar e tentalo de novo. Non axudou. Buscamos en Google CRC32: estes resultaron ser obxectos do esquema 73, aínda que traballamos no 82. Observamos con atención os rexistros: hai identificadores de dous esquemas diferentes!

Quizais o problema estea exclusivamente no noso cliente non oficial? Non, lanzamos Telegram Desktop 1.2.17 (versión proporcionada en varias distribucións de Linux), escribe no rexistro de excepcións: MTP ID de tipo inesperado #b5223b0f lido en MTPMessageMedia...

Crítica ao protocolo e aos enfoques organizativos de Telegram. Parte 1, técnica: experiencia de escribir un cliente desde cero - TL, MT

Google demostrou que un problema similar xa lle ocorrera a un dos clientes non oficiais, pero despois os números de versión e, en consecuencia, as suposicións eran diferentes...

Entón, que debemos facer? Vasily e mais eu separamos: intentou actualizar o circuíto ao 91, eu decidín esperar uns días e probar o 73. Os dous métodos funcionaron, pero como son empíricos, non hai entendemento de cantas versións hai que subir ou baixar. para saltar, ou canto tempo cómpre esperar.

Máis tarde puiden reproducir a situación: lanzamos o cliente, apagamos, recompilamos o circuíto a outra capa, reiniciamos, detectamos o problema de novo, volvemos ao anterior. uns minutos axudarán. Recibirás unha mestura de estruturas de datos de diferentes capas.

¿Explicación? Como podes adiviñar por varios síntomas indirectos, o servidor consta de moitos procesos de diferentes tipos en diferentes máquinas. O máis probable é que o servidor responsable do "buffering" puxese na cola o que lle deron os seus superiores, e déronllo no esquema que había no momento da xeración. E ata que esta cola "podre", non se puido facer nada.

Quizais... pero isto é unha muleta terrible?!.. Non, antes de pensar en ideas tolas, vexamos o código dos clientes oficiais. Na versión de Android non atopamos ningún analizador TL, pero atopamos un ficheiro voluminoso (GitHub négase a retocalo) con (des)serialización. Aquí están os fragmentos 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 salvaxe. Pero, probablemente, este é código xerado, entón vale?... Pero certamente admite todas as versións! É certo, non está claro por que todo se mestura, chats secretos e todo tipo de cousas _old7 dalgún xeito non semella a xeración de máquinas... Porén, sobre todo, quedei abraiado

TL_message_layer104
TL_message_layer104_2
TL_message_layer104_3

Rapaces, non podedes decidir o que hai dentro dunha capa? Ben, vale, digamos que "dous" foron liberados cun erro, bueno, pasa, pero TRES?... Enseguida, o mesmo anciño outra vez? Que tipo de pornografía é esta, perdón?...

No código fonte de Telegram Desktop, por certo, ocorre unha cousa semellante: se é así, varios compromisos seguidos no esquema non cambian o seu número de capa, senón que arranxan algo. En condicións en que non exista unha fonte oficial de datos para o esquema, de onde se pode obter, agás o código fonte do cliente oficial? E se o tomas de alí, non podes estar seguro de que o esquema sexa completamente correcto ata que probes todos os métodos.

Como se pode probar isto? Espero que os fanáticos das probas unitarias, funcionais e doutras probas compartan nos comentarios.

Vale, vexamos outra peza 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 comentario "creado manualmente" suxire que só parte deste ficheiro se escribiu manualmente (imaxinades todo o pesadelo de mantemento?), e o resto foi xerado pola máquina. Non obstante, xorde outra cuestión: que as fontes estean dispoñibles non completamente (a la GPL blobs no núcleo Linux), pero este xa é un tema para a segunda parte.

Pero abonda. Pasemos ao protocolo sobre o que se executa toda esta serialización.

MT Proto

Entón, imos abrir descrición xeral и descrición detallada do protocolo e o primeiro que tropezamos é a terminoloxía. E con abundancia de todo. En xeral, esta parece ser unha característica propietaria de Telegram: chamar as cousas de forma diferente en diferentes lugares, ou cousas diferentes cunha palabra ou viceversa (por exemplo, nunha API de alto nivel, se ves un paquete de adhesivos, non o é. o que pensaches).

Por exemplo, "mensaxe" e "sesión" significan algo diferente aquí que na interface habitual do cliente de Telegram. Ben, todo está claro coa mensaxe, pódese interpretar en termos de POO ou simplemente chamar a palabra "paquete": este é un nivel de transporte baixo, non hai as mesmas mensaxes que na interface, hai moitas mensaxes de servizo. . Pero a sesión... pero primeiro o primeiro.

capa de transporte

O primeiro é o transporte. Falanos de 5 opcións:

  • TCP
  • Websocket
  • Websocket a través de HTTPS
  • HTTP
  • HTTPS

Vasily, [15.06.18 15:04] Tamén hai transporte UDP, pero non está documentado

E TCP en tres variantes

O primeiro é semellante a UDP sobre TCP, cada paquete inclúe un número de secuencia e crc
Por que é tan doloroso ler documentos nun carro?

Ben, aí está agora TCP xa en 4 variantes:

  • Abreviado
  • Intermedio
  • Intermedio acolchado
  • Completo

Ben, ok, Padded intermediate para MTProxy, isto engadiuse máis tarde debido a eventos coñecidos. Pero, ¿por que dúas versións máis (tres en total) cando te podías arreglar cunha? Os catro difiren esencialmente só en como configurar a lonxitude e a carga útil do MTProto principal, que se comentará máis adiante:

  • en Abreviado é 1 ou 4 bytes, pero non 0xef, entón o corpo
  • en Intermedio isto é de 4 bytes de lonxitude e un campo, e a primeira vez que o cliente debe enviar 0xeeeeeeee para indicar que é Intermedio
  • en Pleno o máis adictivo, desde o punto de vista dun networker: lonxitude, número de secuencia, e NON O que é principalmente MTProto, body, CRC32. Si, todo isto está enriba de TCP. O que nos proporciona un transporte fiable en forma de fluxo de bytes secuencial; non se necesitan secuencias, especialmente sumas de verificación. Está ben, agora alguén oporá que TCP ten unha suma de verificación de 16 bits, polo que se produce a corrupción dos datos. Xenial, pero en realidade temos un protocolo criptográfico con hash de máis de 16 bytes, todos estes erros, e aínda máis, serán detectados por unha falta de coincidencia SHA nun nivel superior. Non hai ningún punto en CRC32 por riba disto.

Comparemos Abreviado, no que é posible un byte de lonxitude, con Intermedio, que xustifica "No caso de que sexa necesario un aliñamento de datos de 4 bytes", o que é un disparate. Crese que os programadores de Telegram son tan incompetentes que non poden ler os datos dun socket nun búfer aliñado? Aínda tes que facelo, porque a lectura pode devolverche calquera número de bytes (e tamén hai servidores proxy, por exemplo...). Ou, por outra banda, por que bloquear Abreviado se aínda teremos un gran recheo por riba de 16 bytes? garda 3 bytes ás veces ?

Un ten a impresión de que a Nikolai Durov gústalle moito reinventar as rodas, incluídos os protocolos de rede, sen necesidade práctica real.

Outras opcións de transporte, incl. Web e MTProxy, non imos considerar agora, quizais noutra publicación, se hai unha solicitude. Sobre este mesmo MTProxy, só lembremos agora que pouco despois do seu lanzamento en 2018, os provedores aprenderon rapidamente a bloquealo, destinado a bloqueo de bypassPor tamaño do paquete! E tamén o feito de que o servidor MTProxy escrito (de novo por Waltman) en C estaba demasiado ligado ás especificidades de Linux, aínda que non era necesario en absoluto (Phil Kulin confirmará), e que un servidor similar en Go ou Node.js caben en menos de cen liñas.

Pero sobre a alfabetización técnica destas persoas sacaremos conclusións ao final do apartado, despois de considerar outras cuestións. Polo momento, pasemos á capa OSI 5, sesión, na que colocaron a sesión MTProto.

Claves, mensaxes, sesións, Diffie-Hellman

Non a colocaron alí correctamente... Unha sesión non é a mesma sesión visible na interface en Sesións activas. Pero en orde.

Crítica ao protocolo e aos enfoques organizativos de Telegram. Parte 1, técnica: experiencia de escribir un cliente desde cero - TL, MT

Entón recibimos unha cadea de bytes de lonxitude coñecida da capa de transporte. Esta é unha mensaxe cifrada ou texto plano, se aínda estamos na fase de acordo de clave e realmente o estamos facendo. De cal dos conceptos chamados "clave" estamos a falar? Imos aclarar esta cuestión para o propio equipo de Telegram (desculpen por traducir a miña propia documentación do inglés co cerebro canso ás 4 da mañá, foi máis fácil deixar algunhas frases tal e como están):

Hai dúas entidades chamadas Sesión - un na IU dos clientes oficiais en "sesións actuais", onde cada sesión corresponde a un dispositivo/SO completo.
O segundo é Sesión MTProto, que ten o número de secuencia da mensaxe (nun sentido de baixo nivel) e cal pode durar entre diferentes conexións TCP. Pódense instalar varias sesións de MTProto ao mesmo tempo, por exemplo, para acelerar a descarga de ficheiros.

Entre estes dous sesións hai un concepto autorización. No caso dexenerado, podemos dicir que Sesión de IU é o mesmo que autorización, pero por desgraza, todo é complicado. Vexamos:

  • O usuario do novo dispositivo xera primeiro clave_auth e limítao á conta, por exemplo a través de SMS, por iso autorización
  • Ocorreu dentro do primeiro Sesión MTProto, que ten session_id dentro de ti.
  • Neste paso, a combinación autorización и session_id podería chamarse exemplo - esta palabra aparece na documentación e código dalgúns clientes
  • Entón, o cliente pode abrir algúns Sesións MTProto baixo o mesmo clave_auth - ao mesmo DC.
  • Entón, un día o cliente terá que solicitar o ficheiro outro DC - e para este DC xerarase un novo clave_auth !
  • Informar ao sistema de que non se rexistra un usuario novo, senón o mesmo autorización (Sesión de IU), o cliente usa chamadas API auth.exportAuthorization na casa DC auth.importAuthorization no novo DC.
  • Todo é igual, varios poden estar abertos Sesións MTProto (cada un co seu session_id) a este novo DC, baixo súa clave_auth.
  • Finalmente, o cliente pode querer Perfect Forward Secrecy. Cada clave_auth foi permanente clave - por DC - e o cliente pode chamar auth.bindTempAuthKey para o seu uso temporal clave_auth - e de novo, só un chave_auth_temp por DC, común a todos Sesións MTProto a este DC.

Teña en conta que sal (e futuros sales) tamén é un clave_auth aqueles. compartida entre todos Sesións MTProto ao mesmo DC.

Que significa "entre diferentes conexións TCP"? Entón isto significa algo así cookie de autorización nun sitio web: persiste (sobrevive) moitas conexións TCP a un determinado servidor, pero un día vai mal. Só a diferenza de HTTP, en MTProto as mensaxes dentro dunha sesión numéranse e confírmanse secuencialmente; se entraron no túnel, a conexión rompíase; despois de establecer unha nova conexión, o servidor enviará todo o que non entregou na sesión anterior. Conexión TCP.

Non obstante, a información anterior resúmese despois de moitos meses de investigación. Mentres tanto, estamos implementando o noso cliente desde cero? - volvamos ao principio.

Entón imos xerar auth_key en Versións Diffie-Hellman de Telegram. Intentemos entender a documentación...

Vasily, [19.06.18 20:05] data_with_hash := SHA1(datos) + data + (calquera bytes aleatorios); tal que a lonxitude sexa igual a 255 bytes;
datos_cifrados := RSA(datos_con_hash, clave_pública_servidor); un número de 255 bytes (big endian) elévase á potencia necesaria sobre o módulo necesario e o resultado gárdase como un número de 256 bytes.

Teñen algo de droga DH

Non parece o DH dunha persoa sa
Non hai dúas chaves públicas en dx

Ben, ao final, isto resolveuse, pero quedou un residuo: o cliente fai unha proba de traballo de que foi capaz de factorizar o número. Tipo de protección contra ataques DoS. E a clave RSA só se usa unha vez nunha dirección, esencialmente para o cifrado new_nonce. Pero aínda que esta operación aparentemente sinxela terá éxito, a que terás que enfrontarte?

Vasily, [20.06.18/00/26 XNUMX:XNUMX] Aínda non cheguei á solicitude de aplicación

Enviei esta solicitude a DH

E, no peirao de transporte di que pode responder con 4 bytes dun código de erro. Iso é todo

Ben, díxome -404, e que?

Entón díxenlle: "Pilla a túa merda cifrada cunha chave de servidor cunha pegada como esta, quero DH", e respondeu cun estúpido 404.

Que pensarías desta resposta do servidor? Que facer? Non hai a quen preguntar (pero máis sobre iso na segunda parte).

Aquí todo o interese faise no peirao

Non teño outra cousa que facer, só soñei con converter números de ida e volta

Dous números de 32 bits. Empaqueinos coma todos

Pero non, estes dous hai que engadir primeiro á liña como BE

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

Vasily, [20.06.18 15:49] SI!

Vadim Goncharov, [20.06.18 15:50] polo que non entendo o que pode "non atopou"

Vasily, [20.06.18 15:50] aproximadamente

Non puiden atopar tal descomposición en factores primos%)

Nin sequera xestionamos os informes de erros

Vasily, [20.06.18 20:18] Ah, tamén hai MD5. Xa hai tres hashes diferentes

A pegada dixital clave calcúlase do seguinte xeito:

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

SHA1 e sha2

Entón imos poñelo auth_key recibimos 2048 bits de tamaño usando Diffie-Hellman. Que segue? A continuación descubrimos que os 1024 bits inferiores desta chave non se usan de ningún xeito... pero pensemos nisto por agora. Neste paso, temos un segredo compartido co servidor. Estableceuse un análogo da sesión TLS, que é un procedemento moi caro. Pero o servidor aínda non sabe nada de quen somos! Aínda non, en realidade. autorización. Eses. se pensaches en termos de "contrasinal de inicio de sesión", como xa fixeches en ICQ, ou polo menos "clave de inicio de sesión", como en SSH (por exemplo, nalgúns gitlab/github). Recibimos un anónimo. E se o servidor nos di "estes números de teléfono son atendidos por outro DC"? Ou incluso "o teu número de teléfono está prohibido"? O mellor que podemos facer é manter a chave coa esperanza de que sexa útil e non se podre para entón.

Por certo, "recibimos" con reservas. Por exemplo, confiamos no servidor? E se é falso? Serían necesarios verificacións criptográficas:

Vasily, [21.06.18 17:53] Ofrecen aos clientes móbiles comprobar un número de 2 kbit para a primalidade%)

Pero non está nada claro, nafeijoa

Vasily, [21.06.18 18:02] O documento non di que facer se resulta que non é sinxelo

Non dito. Imos ver que fai o cliente oficial de Android neste caso? A iso é o que (e si, todo o ficheiro é interesante) - como din, só deixarei isto aquí:

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

Non, claro que aínda está aí algunhas Hai probas para a primalidade dun número, pero persoalmente xa non teño coñecementos suficientes de matemáticas.

Está ben, temos a chave mestra. Para iniciar sesión, é dicir. enviar solicitudes, cómpre realizar un cifrado adicional mediante AES.

A clave de mensaxe defínese como os 128 bits intermedios do SHA256 do corpo da mensaxe (incluíndo sesión, ID da mensaxe, etc.), incluíndo os bytes de recheo, antepostos por 32 bytes tomados da clave de autorización.

Vasily, [22.06.18 14:08] Media, cadela, bits

Recibido auth_key. Todos. Máis aló deles... non se desprende do documento. Non dubides en estudar o código fonte aberto.

Teña en conta que MTProto 2.0 require de 12 a 1024 bytes de recheo, aínda suxeito á condición de que a lonxitude da mensaxe resultante sexa divisible por 16 bytes.

Entón, canto recheo deberías engadir?

E si, tamén hai un 404 en caso de erro

Se alguén estudou coidadosamente o diagrama e o texto da documentación, decatouse de que alí non hai MAC. E ese AES úsase nun determinado modo IGE que non se usa en ningún outro lugar. Eles, por suposto, escriben sobre isto nas súas preguntas frecuentes... Aquí, como, a clave da mensaxe en si é tamén o hash SHA dos datos descifrados, que se usa para comprobar a integridade e, en caso de que non coincida, a documentación por algún motivo. recomenda ignoralos en silencio (pero que pasa coa seguridade, e se nos rompen?).

Non son criptógrafo, quizais non teña nada de malo neste modo neste caso desde o punto de vista teórico. Pero podo nomear claramente un problema práctico, usando Telegram Desktop como exemplo. Cifra a caché local (todos estes D877F783D5D3EF8C) do mesmo xeito que as mensaxes en MTProto (só neste caso a versión 1.0), é dicir. primeiro a clave da mensaxe, despois os propios datos (e nalgún lugar aparte do principal auth_key 256 bytes, sen os cales msg_key inútil). Así, o problema faise perceptible en ficheiros grandes. É dicir, cómpre manter dúas copias dos datos: cifradas e descifradas. E se hai megabytes, ou vídeo en streaming, por exemplo?... Os esquemas clásicos con MAC despois do texto cifrado permiten lelo en fluxo, transmitilo inmediatamente. Pero con MTProto terás que facelo ao principio cifrar ou descifrar a mensaxe completa, só despois transfire á rede ou ao disco. Polo tanto, nas últimas versións de Telegram Desktop na caché user_data Tamén se usa outro formato, con AES no modo CTR.

Vasily, [21.06.18 01:27] Ah, descubrín o que é IGE: IGE foi o primeiro intento dun "modo de cifrado de autenticación", orixinalmente para Kerberos. Foi un intento fallido (non ofrece protección de integridade) e tivo que ser eliminado. Ese foi o comezo dunha busca de 20 anos por un modo de cifrado de autenticación que funcione, que culminou recentemente en modos como OCB e GCM.

E agora os argumentos do lado do carro:

O equipo detrás de Telegram, dirixido por Nikolai Durov, está formado por seis campións ACM, a metade deles doutores en matemáticas. Tardaron uns dous anos en lanzar a versión actual de MTProto.

É divertido. Dous anos no nivel inferior

Ou só podes tomar tls

Está ben, digamos que fixemos o cifrado e outros matices. É posible finalmente enviar solicitudes seriadas en TL e deserializar as respostas? Entón, que e como debes enviar? Aquí, digamos, o método initConnection, quizais sexa isto?

Vasily, [25.06.18 18:46] Inicializa a conexión e garda información no dispositivo e aplicación do usuario.

Acepta app_id, device_model, system_version, app_version e lang_code.

E algunha consulta

Documentación coma sempre. Non dubides en estudar o código aberto

Se todo estaba aproximadamente claro con invokeWithLayer, que pasa aquí? Resulta que, digamos que temos -o cliente xa tiña algo que preguntarlle ao servidor- hai unha solicitude que queriamos enviar:

Vasily, [25.06.18 19:13] A xulgar polo código, a primeira chamada está envolta nesta merda, e a propia merda está envolta en invokewithlayer

Por que initConnection non podería ser unha chamada separada, pero debe ser un envoltorio? Si, como se viu, hai que facelo cada vez ao comezo de cada sesión, e non unha vez, como ocorre coa tecla principal. Pero! Non pode ser chamado por un usuario non autorizado! Agora chegamos á fase na que é aplicable Este páxina de documentación - e dinos que...

Só unha pequena parte dos métodos da API están dispoñibles para usuarios non autorizados:

  • auth.sendCode
  • auth.resendCode
  • account.getPassword
  • auth.checkPassword
  • auth.checkPhone
  • auth.signUp
  • auth.signIn
  • auth.importAuthorization
  • help.getConfig
  • help.getNearestDc
  • help.getAppUpdate
  • help.getCdnConfig
  • langpack.getLangPack
  • langpack.getStrings
  • langpack.getDifference
  • langpack.getLanguages
  • langpack.getLanguage

O primeiro deles, auth.sendCode, e está esa querida primeira solicitude na que enviamos api_id e api_hash, e despois recibimos unha SMS cun código. E se estamos no DC equivocado (os números de teléfono deste país son atendidos por outro, por exemplo), entón recibiremos un erro co número do DC desexado. Para saber a que enderezo IP por número de DC tes que conectar, axúdanos help.getConfig. No seu momento só había 5 entradas, pero despois dos famosos eventos de 2018, o número aumentou significativamente.

Agora lembremos que chegamos a esta fase no servidor de forma anónima. Non é moi caro obter só un enderezo IP? Por que non facer isto e outras operacións na parte sen cifrar de MTProto? Escoito a obxección: "como podemos asegurarnos de que non é RKN quen responderá con enderezos falsos?" A isto lembramos que, en xeral, os clientes oficiais As claves RSA están incrustadas, é dicir. podes só asinar esta información. En realidade, isto xa se está a facer para obter información sobre evitar o bloqueo que os clientes reciben a través doutras canles (loxicamente, isto non se pode facer no propio MTProto; tamén cómpre saber onde conectarse).

OK. Nesta fase de autorización do cliente, aínda non estamos autorizados e non rexistramos a nosa aplicación. Só queremos ver por agora o que responde o servidor aos métodos dispoñibles para un usuario non autorizado. E aquí…

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

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

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

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

No esquema, o primeiro é o segundo

No esquema tdesktop o terceiro valor é

Si, dende entón, claro, a documentación foi actualizada. Aínda que pronto pode volver a ser irrelevante. Como debería saber un desenvolvedor novato? Quizais se rexistras a túa solicitude, o informarán? Vasily fixo isto, pero por desgraza, non lle enviaron nada (de novo, falaremos disto na segunda parte).

...Notaches que xa nos movemos dalgún xeito á API, é dicir. ao seguinte nivel e perdeches algo no tema MTProto? Sen sorpresa:

Vasily, [28.06.18 02:04] Mm, están rebuscando nalgúns dos algoritmos de e2e

Mtproto define algoritmos de cifrado e claves para ambos dominios, así como un pouco de estrutura de envoltorio

Pero mesturan constantemente diferentes niveis da pila, polo que non sempre está claro onde rematou mtproto e onde comezou o seguinte nivel.

Como se mesturan? Ben, aquí está a mesma clave temporal para PFS, por exemplo (por certo, Telegram Desktop non pode facelo). É executado por unha solicitude API auth.bindTempAuthKey, é dicir. dende o nivel superior. Pero ao mesmo tempo interfire co cifrado no nivel inferior; despois, por exemplo, cómpre facelo de novo initConnection etc., isto non é xusto petición normal. O que tamén é especial é que só podes ter UNHA chave temporal por DC, aínda que o campo auth_key_id en cada mensaxe permítelle cambiar a chave polo menos en todas as mensaxes, e que o servidor ten dereito a "esquecer" a chave temporal en calquera momento; a documentación non di que facer neste caso... ben, por que non podería Non tes varias claves, como ocorre cun conxunto de sales futuras, e?...

Hai algunhas outras cousas que vale a pena destacar sobre o tema MTProto.

Mensaxes de mensaxes, msg_id, msg_seqno, confirmacións, pings na dirección incorrecta e outras idiosincrasias

Por que necesitas saber sobre eles? Porque "filtran" a un nivel superior e cómpre ter en conta deles cando traballes coa API. Supoñamos que non estamos interesados ​​en msg_key; o nivel inferior descifrounos todo. Pero dentro dos datos descifrados temos os seguintes campos (tamén a lonxitude dos datos, polo que sabemos onde está o recheo, pero iso non é importante):

  • sal - int64
  • session_id - int64
  • mensaxe_id — int64
  • seq_no - int32

Lembrámosvos que só hai un sal para toda a DC. Por que saber dela? Non só porque haxa unha solicitude get_future_salts, que che indica que intervalos serán válidos, pero tamén porque se o teu sal está "podre", entón a mensaxe (solicitude) simplemente perderase. O servidor, por suposto, informará da nova sal emitindo new_session_created - pero co vello terás que reenvialo dalgún xeito, por exemplo. E este problema afecta á arquitectura da aplicación.

Permítese ao servidor abandonar sesións por completo e responder deste xeito por moitos motivos. En realidade, que é unha sesión MTProto do lado do cliente? Estes son dous números session_id и seq_no mensaxes nesta sesión. Ben, e a conexión TCP subxacente, por suposto. Digamos que o noso cliente aínda non sabe facer moitas cousas, desconectouse, volveu conectar. Se isto ocorreu rapidamente - a sesión antiga continuou na nova conexión TCP, aumenta seq_no máis aló. Se leva moito tempo, o servidor podería borralo, porque polo seu lado tamén é unha cola, como descubrimos.

Que debería ser seq_no? Ah, esa é unha pregunta complicada. Tenta entender honestamente o que quería dicir:

Mensaxe relacionada co contido

Unha mensaxe que require un recoñecemento explícito. Estes inclúen todas as mensaxes do usuario e moitas mensaxes de servizo, practicamente todas a excepción dos contedores e os recoñecementos.

Número de secuencia de mensaxes (msg_seqno)

Un número de 32 bits igual ao dobre do número de mensaxes "relacionadas co contido" (aquelas que requiren recoñecemento, e en particular as que non son contedores) creadas polo remitente antes desta mensaxe e incrementadas posteriormente nun un se a mensaxe actual é un mensaxe relacionada co contido. Un contedor xérase sempre despois de todo o seu contido; polo tanto, o seu número de secuencia é maior ou igual aos números de secuencia das mensaxes contidas nel.

Que tipo de circo é este cun incremento en 1 e despois outro en 2?... Sospeito que inicialmente significaban "o bit menos significativo para ACK, o resto é un número", pero o resultado non é o mesmo - en particular, sae, pódese enviar algúns confirmacións tendo o mesmo seq_no! Como? Pois, por exemplo, o servidor envíanos algo, envíano, e nós mesmos ficamos calados, só respondendo con mensaxes de servizo confirmando a recepción das súas mensaxes. Neste caso, as nosas confirmacións de saída terán o mesmo número de saída. Se estás familiarizado co TCP e pensas que isto soa dalgún xeito salvaxe, pero non parece moi salvaxe, porque en TCP seq_no non cambia, pero a confirmación vai a seq_no do outro lado, apresurareime a molestarte. As confirmacións son proporcionadas en MTProto NON en seq_no, como en TCP, pero por msg_id !

Que é isto msg_id, o máis importante destes campos? Un identificador de mensaxe único, como o nome indica. Defínese como un número de 64 bits, cuxos bits máis baixos teñen de novo a maxia "servidor-non-servidor", e o resto é unha marca de tempo de Unix, incluída a parte fraccionaria, desprazada 32 bits á esquerda. Eses. marca de tempo per se (e as mensaxes con tempos que difiren demasiado serán rexeitadas polo servidor). Disto resulta que en xeral este é un identificador que é global para o cliente. Tendo en conta que - lembremos session_id -Temos garantido: En ningún caso unha mensaxe destinada a unha sesión pode enviarse a unha sesión diferente. É dicir, resulta que xa hai tres nivel: sesión, número de sesión, ID da mensaxe. Por que tal complicación, este misterio é moi grande.

Así, msg_id necesario para...

RPC: solicitudes, respostas, erros. Confirmacións.

Como podes ter notado, non hai ningún tipo ou función especial de "facer unha solicitude RPC" en ningún lugar do diagrama, aínda que hai respostas. Despois de todo, temos mensaxes relacionadas co contido! É dicir, calquera a mensaxe pode ser unha solicitude! Ou non ser. Despois de todo, de cada un ten msg_id. Pero hai respostas:

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

Aquí é onde se indica a que mensaxe é unha resposta. Polo tanto, no nivel superior da API, terás que lembrar cal era o número da túa solicitude; creo que non hai que explicar que o traballo é asíncrono e que pode haber varias solicitudes en curso ao mesmo tempo. as respostas ás que se poden devolver en calquera orde? En principio, a partir desta e das mensaxes de erro como non hai traballadores, pódese rastrexar a arquitectura detrás desta: o servidor que mantén unha conexión TCP contigo é un equilibrador front-end, reenvía as solicitudes aos backends e recóllaas a través de message_id. Parece que aquí todo é claro, lóxico e bo.

Si?.. E se o pensas? Despois de todo, a propia resposta RPC tamén ten un campo msg_id! Necesitamos berrarlle ao servidor "non respondes á miña resposta!"? E si, que había sobre as confirmacións? Sobre a páxina mensaxes sobre mensaxes dinos o que é

msgs_ack#62d6b459 msg_ids:Vector long = MsgsAck;

e hai que facelo por cada lado. Pero non sempre! Se recibiu un RpcResult, serve como confirmación. É dicir, o servidor pode responder á túa solicitude con MsgsAck, como "Recibíno". RpcResult pode responder inmediatamente. Poderían ser os dous.

E si, aínda tes que responder a resposta! Confirmación. En caso contrario, o servidor considerará que non se pode entregar e enviarao de novo. Mesmo despois da reconexión. Pero aquí, por suposto, xorde a cuestión dos tempos mortos. Vexámolos un pouco máis tarde.

Mentres tanto, vexamos posibles erros de execución de consultas.

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

Oh, alguén vai exclamar, aquí está un formato máis humano - hai unha liña! Tómate o teu tempo. Aquí lista de erros, pero por suposto non completo. Del aprendemos que o código é algo así Erros HTTP (ben, por suposto, non se respecta a semántica das respostas, nalgúns lugares distribúense de forma aleatoria entre os códigos), e a liña parece CAPITAL_LETTERS_AND_NUMBERS. Por exemplo, PHONE_NUMBER_OCCUPIED ou FILE_PART_Х_MISSING. Ben, é dicir, aínda necesitarás esta liña analizar. Por exemplo FLOOD_WAIT_3600 significará que tes que esperar unha hora, e PHONE_MIGRATE_5, que un número de teléfono con este prefixo debe estar rexistrado no 5o DC. Temos unha linguaxe tipo, non? Non necesitamos un argumento dunha cadea, os normais servirán, está ben.

De novo, isto non está na páxina de mensaxes de servizo, pero, como xa é habitual neste proxecto, a información pódese atopar noutra páxina de documentación. Ou botar sospeitas. En primeiro lugar, mira, tecleo/violación da capa - RpcError pódese aniñar RpcResult. Por que non fóra? Que non tivemos en conta?.. En consecuencia, onde está a garantía de que RpcError NON pode estar incrustado RpcResult, pero estar directamente ou aniñado noutro tipo?.. E se non pode, por que non está no nivel superior, é dicir. falta req_msg_id ? ..

Pero sigamos coas mensaxes de servizo. O cliente pode pensar que o servidor está pensando durante moito tempo e facer esta marabillosa solicitude:

rpc_drop_answer#58e4a740 req_msg_id:long = RpcDropAnswer;

Existen tres posibles respostas a esta pregunta, que se cruzan de novo co mecanismo de confirmación; tentar comprender cales deberían ser (e cal é a lista xeral de tipos que non precisan confirmación) déixase ao lector como tarefa para a casa (nota: a información en o código fonte de Telegram Desktop non está completo).

Drogodependencia: estados das mensaxes

En xeral, moitos lugares en TL, MTProto e Telegram en xeral deixan unha sensación de teimosía, pero por cortesía, tacto e outros. habilidades suaves Cortésmente gardamos silencio ao respecto, e censuramos as obscenidades dos diálogos. Con todo, este lugarОa maior parte da páxina trata sobre mensaxes sobre mensaxes É chocante mesmo para min, que levo moito tempo traballando con protocolos de rede e vin bicicletas con diversos graos de deformación.

Comeza inocuamente, con confirmacións. A continuación fálannos

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;

Ben, todos os que comecen a traballar con MTProto terán que lidiar con eles; no ciclo "corrixido - recompilado - iniciado", obter erros de número ou sal que conseguiu saír mal durante as edicións é algo común. Non obstante, aquí hai dous puntos:

  1. Isto significa que se perde a mensaxe orixinal. Necesitamos crear algunhas filas, verémolo máis tarde.
  2. Cales son estes números de erro estraños? 16, 17, 18, 19, 20, 32, 33, 34, 35, 48, 64... onde están os demais números, Tommy?

A documentación indica:

A intención é que os valores de error_code se agrupen (error_code >> 4): por exemplo, os códigos 0x40 — 0x4f corresponden a erros na descomposición do contedor.

pero, en primeiro lugar, un cambio na outra dirección e, en segundo lugar, non importa, onde están os outros códigos? Na cabeza do autor?.. Porén, estas son bagatelas.

A adicción comeza nas mensaxes sobre o estado das mensaxes e as copias das mensaxes:

  • Solicitude de información sobre o estado da mensaxe
    Se algunha das partes non recibiu información sobre o estado das súas mensaxes saíntes durante un tempo, pode solicitala expresamente á outra parte:
    msgs_state_req#da69fb52 msg_ids:Vector long = MsgsStateReq;
  • Mensaxe informativa sobre o estado das mensaxes
    msgs_state_info#04deb57d req_msg_id:long info:string = MsgsStateInfo;
    Aquí, info é unha cadea que contén exactamente un byte de estado da mensaxe para cada mensaxe da lista de msg_ids entrante:

    • 1 = non se sabe nada sobre a mensaxe (msg_id moi baixo, é posible que a outra parte o esquecera)
    • 2 = mensaxe non recibida (msg_id entra dentro do rango de identificadores almacenados; non obstante, a outra parte certamente non recibiu unha mensaxe así)
    • 3 = mensaxe non recibida (msg_id demasiado alto; non obstante, a outra parte aínda non a recibiu)
    • 4 = mensaxe recibida (ten en conta que esta resposta tamén é ao mesmo tempo un acuse de recibo)
    • +8 = mensaxe xa confirmada
    • +16 = mensaxe que non require recoñecemento
    • +32 = Consulta RPC contida na mensaxe en proceso de procesamento ou procesamento xa completo
    • +64 = resposta relacionada co contido á mensaxe xa xerada
    • +128 = a outra parte sabe de feito que a mensaxe xa se recibiu
      Esta resposta non require un recoñecemento. É un recoñecemento dos msgs_state_req relevantes, en si mesmo.
      Teña en conta que se de súpeto resulta que a outra parte non ten unha mensaxe que semella que se lle enviou, a mensaxe pode simplemente ser enviada de novo. Aínda que a outra parte reciba dúas copias da mensaxe ao mesmo tempo, ignorarase o duplicado. (Se pasou demasiado tempo e o msg_id orixinal xa non é válido, a mensaxe debe ser envolto en msg_copy).
  • Comunicación voluntaria do estado das mensaxes
    Calquera das partes poderá informar voluntariamente á outra do estado das mensaxes transmitidas pola outra parte.
    msgs_all_info#8cc0d131 msg_ids:Vector long info:string = MsgsAllInfo
  • Comunicación voluntaria ampliada do estado dunha mensaxe
    ...
    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;
  • Solicitude explícita para volver enviar mensaxes
    msg_resend_req#7d861a08 msg_ids:Vector long = MsgResendReq;
    A parte remota responde inmediatamente enviando de novo as mensaxes solicitadas [...]
  • Solicitude explícita para reenviar as respostas
    msg_resend_ans_req#8610baeb msg_ids:Vector long = MsgResendReq;
    A parte remota responde inmediatamente enviando de novo respostas ás mensaxes solicitadas […]
  • Copias de mensaxes
    Nalgunhas situacións, unha mensaxe antiga cun msg_id que xa non é válido debe ser enviada de novo. Despois, envólvese nun recipiente para copias:
    msg_copy#e06046b2 orig_message:Message = MessageCopy;
    Unha vez recibida, a mensaxe procédese coma se o envoltorio non estivese alí. Non obstante, se se sabe con certeza que se recibiu a mensaxe orig_message.msg_id, entón a nova mensaxe non se procesa (mentres, ao mesmo tempo, recoñécense esta e orig_message.msg_id). O valor de orig_message.msg_id debe ser inferior ao msg_id do contedor.

Incluso calemos sobre o que msgs_state_info de novo as orellas do TL inacabado están a saír (necesitabamos un vector de bytes, e nos dous bits inferiores había unha enumeración, e nos dous bits superiores había bandeiras). O punto é diferente. Alguén entende por que todo isto está na práctica? nun cliente real necesario?.. Con dificultade, pero pódese imaxinar algún beneficio se unha persoa se dedica á depuración e nun modo interactivo: pregúntalle ao servidor que e como. Pero aquí descríbense as solicitudes viaxe de ida e volta.

Polo tanto, cada parte debe non só cifrar e enviar mensaxes, senón tamén almacenar datos sobre si mesmos, sobre as respostas a elas, durante un período de tempo descoñecido. A documentación non describe nin os tempos nin a aplicabilidade práctica destas características. de ningún xeito. O máis sorprendente é que realmente se usan no código dos clientes oficiais! Ao parecer, dixéronlles algo que non figuraba na documentación pública. Comprender dende o código por que, xa non é tan sinxelo como no caso de TL: non é unha parte (relativamente) illada loxicamente, senón unha peza ligada á arquitectura da aplicación, é dicir. requirirá moito máis tempo para comprender o código da aplicación.

Pings e tempos. Colas.

De todo, se lembramos as suposicións sobre a arquitectura do servidor (distribución de solicitudes entre os backends), segue algo bastante triste: a pesar de todas as garantías de entrega en TCP (ou se entregan os datos ou se lle informará sobre a lagoa, pero os datos entregaranse antes de que ocorra o problema), que as confirmacións no propio MTProto - sen garantías. O servidor pode perder ou tirar facilmente a túa mensaxe e non se pode facer nada ao respecto, só tes que usar diferentes tipos de muletas.

E, en primeiro lugar, colas de mensaxes. Ben, cunha cousa todo era obvio desde o principio: unha mensaxe non confirmada debe ser almacenada e reenviada. E despois de que tempo? E o bufón coñéceo. Quizais esas mensaxes de servizo adictas resolvan dalgún xeito este problema con muletas, por exemplo, en Telegram Desktop hai unhas 4 filas que lles corresponden (quizais máis, como xa se mencionou, para iso hai que afondar máis en serio no seu código e arquitectura; ao mesmo tempo). tempo, sabemos que non se pode tomar como mostra; nel non se usan certos tipos do esquema MTProto).

Por que está a suceder isto? Probablemente, os programadores do servidor non puideron garantir a fiabilidade dentro do clúster, nin sequera almacenar o búfer no equilibrador frontal, e transferiron este problema ao cliente. Por desesperación, Vasily intentou implementar unha opción alternativa, con só dúas filas, utilizando algoritmos de TCP: medindo o RTT ao servidor e axustando o tamaño da "xanela" (en mensaxes) dependendo do número de solicitudes non confirmadas. É dicir, unha heurística tan aproximada para avaliar a carga do servidor é cantas das nosas solicitudes pode mastigar ao mesmo tempo e non perder.

Ben, é dicir, entendes, non? Se ten que implementar TCP de novo enriba dun protocolo que se executa sobre TCP, isto indica un protocolo moi mal deseñado.

Ah, si, por que necesitas máis dunha cola, e que significa isto para unha persoa que traballa cunha API de alto nivel? Mira, fas unha solicitude, serialízaa, pero moitas veces non podes enviala inmediatamente. Por que? Porque a resposta será msg_id, que é temporalаSon unha etiqueta, cuxa asignación é mellor pospoñer ata o máis tarde posible - no caso de que o servidor o rexeite debido a un desajuste de tempo entre nós e el (por suposto, podemos facer unha muleta que cambie o noso tempo do presente). ao servidor engadindo un delta calculado a partir das respostas do servidor: os clientes oficiais fan isto, pero é groseiro e inexacto debido ao almacenamento en búfer). Polo tanto, cando realiza unha solicitude cunha chamada de función local desde a biblioteca, a mensaxe pasa polas seguintes etapas:

  1. Atópase nunha cola e agarda o cifrado.
  2. Nomeado msg_id e a mensaxe foi a outra cola - posible reenvío; enviar ao socket.
  3. a) O servidor respondeu MsgsAck: a mensaxe foi entregada, borrámola da "outra cola".
    b) Ou viceversa, algo non lle gustou, respondeu badmsg - reenviar desde "outra cola"
    c) Non se sabe nada, a mensaxe cómpre reenviar desde outra cola, pero non se sabe exactamente cando.
  4. O servidor finalmente respondeu RpcResult - a resposta real (ou erro) - non só entregada, senón tamén procesada.

Quizais, o uso de contedores podería resolver parcialmente o problema. Isto é cando unha morea de mensaxes se empaquetan nunha soa, e o servidor respondeu cunha confirmación a todas á vez, nun msg_id. Pero tamén rexeitará este paquete, se algo saíu mal, na súa totalidade.

E neste punto entran en xogo consideracións non técnicas. Por experiencia, vimos moitas muletas e, ademais, agora veremos máis exemplos de malos consellos e arquitecturas -en tales condicións, paga a pena confiar e tomar esas decisións? A pregunta é retórica (por suposto que non).

De que estamos a falar? Se sobre o tema das "mensaxes de drogas sobre mensaxes" aínda podes especular con obxeccións como "es parvo, non entendiste o noso brillante plan!" (Así que escriba a documentación primeiro, como debería a xente normal, con razóns e exemplos de intercambio de paquetes, despois falaremos), entón os tempos/tempos de espera son unha cuestión puramente práctica e específica, todo aquí é coñecido desde hai moito tempo. Que nos indica a documentación sobre os tempos de espera?

Un servidor adoita recoñecer a recepción dunha mensaxe dun cliente (normalmente, unha consulta RPC) mediante unha resposta RPC. Se unha resposta tarda en chegar, un servidor pode enviar primeiro un acuse de recibo e, algo máis tarde, a propia resposta RPC.

Un cliente normalmente confirma a recepción dunha mensaxe dun servidor (normalmente, unha resposta RPC) engadindo un acuse de recibo á seguinte consulta RPC se non se transmite demasiado tarde (se se xera, por exemplo, 60-120 segundos despois da recepción). dunha mensaxe do servidor). Non obstante, se durante un longo período de tempo non hai motivos para enviar mensaxes ao servidor ou se hai un gran número de mensaxes non recoñecidas do servidor (por exemplo, máis de 16), o cliente transmite un acuse de recibo autónomo.

... Traduzo: nós mesmos non sabemos canto e como o necesitamos, así que supoñamos que sexa así.

E sobre os pings:

Mensaxes de ping (PING/PONG)

ping#7abe77ec ping_id:long = Pong;

Normalmente, unha resposta devólvese á mesma conexión:

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

Estas mensaxes non requiren recoñecementos. Un ping só se transmite como resposta a un ping mentres que un ping pode ser iniciado por calquera dos dous lados.

Peche de conexión diferido + PING

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

Funciona como ping. Ademais, despois de recibir isto, o servidor inicia un temporizador que pechará a conexión actual disconnect_delay segundos máis tarde a menos que reciba unha nova mensaxe do mesmo tipo que restableza automaticamente todos os temporizadores anteriores. Se o cliente envía estes pings unha vez cada 60 segundos, por exemplo, pode establecer disconnect_delay igual a 75 segundos.

Estás tolo?! En 60 segundos, o tren entrará na estación, deixará e recollerá pasaxeiros e volverá perder o contacto no túnel. En 120 segundos, mentres o escoitas, chegará a outro, e é probable que a conexión se rompa. Ben, está claro de onde veñen as pernas: "Escoitei un toque, pero non sei onde está", hai o algoritmo de Nagl e a opción TCP_NODELAY, destinada ao traballo interactivo. Pero, desculpe, mantén o seu valor predeterminado: 200 Millisegundos Se realmente queres representar algo semellante e aforrar nun posible par de paquetes, apágao durante 5 segundos, ou o que sexa o tempo de espera da mensaxe "O usuario está escribindo...". Pero non máis.

E para rematar, pings. É dicir, comprobando a vitalidade da conexión TCP. É divertido, pero hai uns 10 anos escribín un texto crítico sobre o mensaxeiro do dormitorio da nosa facultade: os autores alí tamén fixeron ping ao servidor desde o cliente, e non viceversa. Pero os estudantes de 3º son unha cousa, e unha oficina internacional outra, non?...

En primeiro lugar, un pequeno programa educativo. Unha conexión TCP, en ausencia de intercambio de paquetes, pode vivir durante semanas. Isto é bo e malo, dependendo da finalidade. É bo se tivese unha conexión SSH aberta ao servidor, se levantase do ordenador, reiniciou o enrutador, volvese ao seu lugar: a sesión a través deste servidor non se rompeu (non escribiu nada, non había paquetes) , é conveniente. É malo se hai miles de clientes no servidor, cada un ocupando recursos (¡ola, Postgres!)

Os sistemas de chat/IM caen no segundo caso por un motivo adicional: os estados en liña. Se o usuario "caeu", ten que informar aos seus interlocutores sobre isto. En caso contrario, acabarás cun erro que cometeron os creadores de Jabber (e corrixiron durante 20 anos): o usuario desconectouse, pero seguen escribindo mensaxes para el, crendo que está en liña (que tamén se perderon por completo nestes). minutos antes de que se descubrise a desconexión). Non, a opción TCP_KEEPALIVE, que moitas persoas que non entenden como funcionan os temporizadores TCP introducen aleatoriamente (ao establecer valores salvaxes como decenas de segundos), non axudará aquí; debes asegurarte de que non só o núcleo do SO. da máquina do usuario está viva, pero tamén funciona con normalidade, capaz de responder, e a propia aplicación (cres que non se pode conxelar? Telegram Desktop en Ubuntu 18.04 conxeloume máis dunha vez).

Por iso tes que facer ping servidor cliente, e non viceversa: se o cliente fai isto, se a conexión está rota, o ping non se entregará, o obxectivo non se alcanzará.

Que vemos en Telegram? É exactamente o contrario! Ben, iso é. Formalmente, por suposto, ambos os dous lados poden facer ping. Na práctica, os clientes usan unha muleta ping_delay_disconnect, que configura o temporizador no servidor. Ben, desculpe, non lle corresponde ao cliente decidir canto tempo quere vivir alí sen ping. O servidor, en función da súa carga, sábeo mellor. Pero, por suposto, se non che importan os recursos, entón serás o teu propio Pinocho malvado, e unha muleta servirá...

Como debería ter sido deseñado?

Creo que os feitos anteriores indican claramente que o equipo de Telegram/VKontakte non é moi competente no ámbito do transporte (e nivel inferior) das redes informáticas e as súas baixas cualificacións en materias relevantes.

Por que resultou tan complicado e como poden os arquitectos de Telegram intentar opoñerse? O feito de que tentasen facer unha sesión que sobreviva ás interrupcións da conexión TCP, é dicir, o que non se entregou agora, entregarémolo máis tarde. Probablemente tamén tentaron facer un transporte UDP, pero atoparon dificultades e abandonárono (por iso a documentación está baleira, non había nada de que presumir). Pero debido á falta de comprensión de como funcionan as redes en xeral e TCP en particular, onde pode confiar nela e onde precisa facelo vostede mesmo (e como), e un intento de combinar isto coa criptografía "dous paxaros con unha pedra”, este é o resultado.

Como era necesario? Baseándose no feito de que msg_id é unha marca de tempo necesaria desde o punto de vista criptográfico para evitar ataques de repetición, é un erro engadirlle unha función de identificador único. Polo tanto, sen cambiar fundamentalmente a arquitectura actual (cando se xera o fluxo de Actualizacións, ese é un tema da API de alto nivel para outra parte desta serie de publicacións), habería que:

  1. O servidor que mantén a conexión TCP co cliente asume a responsabilidade: se leu desde o socket, por favor, reconoza, procese ou devolva un erro, sen perdas. Entón, a confirmación non é un vector de identificadores, senón simplemente "o último seq_no recibido" - só un número, como en TCP (dous números - o seu seq e o confirmado). Sempre estamos dentro da sesión, non si?
  2. O selo de tempo para evitar ataques de repetición convértese nun campo separado, á vez. Está comprobado, pero non afecta a nada máis. Abonda e uint32 - se o noso sal cambia polo menos cada medio día, podemos asignar 16 bits aos bits de orde baixa dunha parte enteira do tempo actual, o resto a unha fracción de segundo (como agora).
  3. Eliminado msg_id en absoluto: desde o punto de vista de distinguir as solicitudes nos backends, hai, en primeiro lugar, o ID do cliente e, en segundo lugar, o ID de sesión, concatenalos. En consecuencia, só unha cousa é suficiente como identificador de solicitude seq_no.

Esta tampouco é a opción máis exitosa; un aleatorio completo podería servir como identificador; isto xa se fai na API de alto nivel cando se envía unha mensaxe, por certo. Sería mellor refacer completamente a arquitectura de relativa a absoluta, pero este é un tema para outra parte, non para esta publicación.

API?

¡Daam! Entón, despois de loitar por un camiño cheo de dor e muletas, por fin puidemos enviar calquera solicitude ao servidor e recibir respostas a elas, así como recibir actualizacións do servidor (non como resposta a unha solicitude, senón por el mesmo). envíanos, como PUSH, se alguén está máis claro así).

Atención, agora haberá o único exemplo en Perl no artigo! (para aqueles que non están familiarizados coa sintaxe, o primeiro argumento de bless é a estrutura de datos do obxecto, o segundo é a súa clase):

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

Si, non é un spoiler adrede; se aínda non o leches, vai adiante e faino!

Oh, wai~~... como é isto? Algo moi familiar... quizais esta sexa a estrutura de datos dunha API web típica en JSON, agás que as clases tamén se anexan aos obxectos?...

Así que é como resulta... De que se trata, compañeiros?... Tanto esforzo - e paramos a descansar onde os programadores web. só comezando?...Non sería máis sinxelo só JSON sobre HTTPS?! Que conseguimos a cambio? O esforzo valeu a pena?

Imos avaliar o que nos deu TL+MTProto e cales son as alternativas posibles. Ben, HTTP, que se centra no modelo de solicitude-resposta, é un mal encaixe, pero polo menos algo por riba de TLS?

Serialización compacta. Vendo esta estrutura de datos, semellante a JSON, recordo que hai versións binarias da mesma. Marquemos MsgPack como insuficientemente extensible, pero hai, por exemplo, CBOR - por certo, un estándar descrito en RFC 7049. Destaca polo feito de que define etiquetas, como mecanismo de expansión, e entre xa estandarizados dispoñible:

  • 25 + 256 - substituíndo as liñas repetidas cunha referencia ao número de liña, un método de compresión tan barato
  • 26 - obxecto Perl serializado co nome de clase e argumentos do construtor
  • 27 - Obxecto serializado independente da linguaxe con nome de tipo e argumentos de construtor

Ben, tentei serializar os mesmos datos en TL e en CBOR co empaquetado de cadeas e obxectos activado. O resultado comezou a variar a favor do CBOR desde un megabyte:

cborlen=1039673 tl_len=1095092

Así, conclusión: Existen formatos substancialmente máis sinxelos que non están suxeitos ao problema de falla de sincronización ou identificador descoñecido, cunha eficacia comparable.

Establecemento de conexión rápida. Isto significa cero RTT despois da reconexión (cando a clave xa se xerou unha vez) -aplicable desde a primeira mensaxe MTProto, pero con algunhas reservas- toca o mesmo sal, a sesión non está podre, etc. Que nos ofrece TLS no seu lugar? Cita sobre o tema:

Cando se usa PFS en TLS, tickets de sesión TLS (RFC 5077) para retomar unha sesión cifrada sen volver a negociar as claves e sen almacenar información sobre as claves no servidor. Ao abrir a primeira conexión e crear claves, o servidor cifra o estado de conexión e transmíteo ao cliente (en forma de ticket de sesión). En consecuencia, cando se retoma a conexión, o cliente envía un ticket de sesión, incluída a clave de sesión, de volta ao servidor. O ticket en si está cifrado cunha clave temporal (chave de ticket de sesión), que se almacena no servidor e debe distribuírse entre todos os servidores frontend que procesan SSL en solucións agrupadas.[10]. Así, a introdución dun ticket de sesión pode violar PFS se as claves temporais do servidor están comprometidas, por exemplo, cando se almacenan durante moito tempo (OpenSSL, nginx, Apache almacénanas por defecto durante toda a duración do programa; os sitios populares usan a chave durante varias horas, ata días).

Aquí o RTT non é cero, cómpre intercambiar polo menos ClientHello e ServerHello, despois do cal o cliente pode enviar datos xunto con Finished. Pero aquí debemos lembrar que non temos a Rede, coa súa morea de conexións recentemente abertas, senón un mensaxeiro, cuxa conexión adoita ser unha solicitude máis ou menos longa e relativamente curta a páxinas web: todo é multiplexado. internamente. É dicir, é bastante aceptable se non nos atopamos cun tramo de metro moi malo.

Esqueciches algo máis? Escribe nos comentarios.

Continuará!

Na segunda parte desta serie de publicacións consideraremos cuestións non técnicas, senón organizativas: enfoques, ideoloxía, interface, actitude cara aos usuarios, etc. Con base, non obstante, na información técnica que se presentou aquí.

A terceira parte seguirá analizando o compoñente técnico/experiencia de desenvolvemento. Aprenderás, en particular:

  • continuación do pandemonio coa variedade de tipos de TL
  • cousas descoñecidas sobre canles e supergrupos
  • por que os diálogos son peores que a lista
  • sobre o enderezo de mensaxes absoluto e relativo
  • cal é a diferenza entre a foto e a imaxe
  • como os emoji interfiren co texto en cursiva

e outras muletas! Estade atentos!

Fonte: www.habr.com

Engadir un comentario