En la parte anterior , describimos los enfoques utilizados al escribir un analizador para el esquema MTProto. El artículo resultó ser un poco más general de lo que esperaba, esta vez intentaré contar más sobre los detalles de Telegram.
El cliente Go continúa evolucionando , retrocederemos en el tiempo y recordaremos cómo se escribieron para él el serializador y deserializador de protocolos .
Los basicos
Hay dos formas de deserializar: transmitido y almacenado en búfer. En la práctica, en MTProto no se puede transmitir un mensaje de más de un megabyte, así que elegí la opción con un búfer: digamos que siempre podemos mantener un mensaje completo en la memoria.
Obtienes la siguiente estructura:
// Buffer implements low level binary (de-)serialization for TL.
type Buffer struct {
Buf []byte
}
Y, sin embargo, MTProto básicamente alinea los valores en 4 bytes (32 bits), pongamos esto en una constante:
// Word represents 4-byte sequence.
// Values in TL are generally aligned to Word.
const Word = 4
Publicación por entregas
Sabiendo que casi todo en MTProto es little-endian, podemos comenzar serializando uint32:
// PutUint32 serializes unsigned 32-bit integer.
func (b *Buffer) PutUint32(v uint32) {
t := make([]byte, Word)
binary.LittleEndian.PutUint32(t, v)
b.Buf = append(b.Buf, t...)
}
Serializaremos todos los demás valores de la misma manera: primero, asignamos el segmento (el compilador Go es lo suficientemente inteligente como para no ponerlo en el montón en este caso, ya que el tamaño del segmento es pequeño y constante), luego escribimos el valor allí, y luego agregó el segmento al búfer.
, , . , grammers, Rust Telegram.
, , , gotd/td/bin .
uint32:
// Uint32 decodes unsigned 32-bit integer from Buffer.
func (b *Buffer) Uint32() (uint32, error) {
if len(b.Buf) < Word {
return 0, io.ErrUnexpectedEOF
}
v := binary.LittleEndian.Uint32(b.Buf)
b.Buf = b.Buf[Word:]
return v, nil
}
, , io.ErrUnexpectedEOF
. . .
([]byte
string
) - 4 .
253, , :
b = append(b, byte(l))
b = append(b, v...)
currentLen := l + 1
// Padding:
b = append(b, make([]byte, nearestPaddedValueLength(currentLen)-currentLen)...)
return b
, 254, little-endian, :
b = append(b, 254, byte(l), byte(l>>8), byte(l>>16))
b = append(b, v...)
currentLen := l + 4
// Padding:
b = append(b, make([]byte, nearestPaddedValueLength(currentLen)-currentLen)...)
encodeString(b []byte, v string) []byte
b
, :
// PutString serializes bare string.
func (b *Buffer) PutString(s string) {
b.Buf = encodeString(b.Buf, s)
}
, , . : ID ( #5b38c6c1
, uint32), , .
, ( ):
// msg#9bdd8f1a code:int32 message:string = Message;
type Message struct {
Code int32
Message string
}
c Buffer
:
// EncodeTo implements bin.Encoder.
func (m Message) Encode(b *Buffer) error {
b.PutID(0x9bdd8f1a)
b.PutInt32(m.Code)
b.PutString(m.Message)
return nil
}
Encode, :
m := Message{
Code: 204,
Message: "Wake up, Neo",
}
b := new(Buffer)
_ = m.Encode(b)
raw := []byte{
// Type ID.
0x1a, 0x8f, 0xdd, 0x9b,
// Code as int32.
204, 0x00, 0x00, 0x00,
// String length.
byte(len(m.Message)),
// "Wake up, Neo" in hex.
0x57, 0x61, 0x6b,
0x65, 0x20, 0x75, 0x70,
0x2c, 0x20, 0x4e, 0x65,
0x6f, 0x00, 0x00, 0x00,
}
, . Buf, :
// PeekID returns next type id in Buffer, but does not consume it.
func (b *Buffer) PeekID() (uint32, error) {
if len(b.Buf) < Word {
return 0, io.ErrUnexpectedEOF
}
v := binary.LittleEndian.Uint32(b.Buf)
return v, nil
}
ConsumeID(id uint32)
: PeekID
, . :
func (m *Message) Decode(b *Buffer) error {
if err := b.ConsumeID(0x9bdd8f1a); err != nil {
return err
}
{
v, err := b.Int32()
if err != nil {
return err
}
m.Code = v
}
{
v, err := b.String()
if err != nil {
return err
}
m.Message = v
}
return nil
}
(-) , :
// Encoder can encode it's binary form to Buffer.
type Encoder interface {
Encode(b *Buffer) error
}
// Decoder can decode it's binary form from Buffer.
type Decoder interface {
Decode(b *Buffer) error
}
, .
. :
messageActionChatCreate#a6638b9a title:string users:Vector<int> = MessageAction;
, title, users?
Vector :
vector#0x1cb5c415 {t:Type} # [ t ] = Vector t
. , , .
: (0x1cb5c415), , :
// PutVectorHeader serializes vector header with provided length.
func (b *Buffer) PutVectorHeader(length int) {
b.PutID(TypeVector)
b.PutInt32(int32(length))
}
, 10 uint32, PutVectorHeader(10)
, 10 uint32.
, , :
boolTrue#997275b5 = Bool; boolFalse#bc799737 = Bool;
, Bool, 0x997275b5, 0xbc799737:
const (
TypeTrue = 0x997275b5 // boolTrue#997275b5 = Bool
TypeFalse = 0xbc799737 // boolFalse#bc799737 = Bool
)
// PutBool serializes bare boolean.
func (b *Buffer) PutBool(v bool) {
switch v {
case true:
b.PutID(TypeTrue)
case false:
b.PutID(TypeFalse)
}
}
, , , .
, , . : , (-), , .
(flags.0?true
): , 0x997275b5
, .
! flags.0?Bool
, Bool, , . , legacy.
bitfield Go :
// Fields represent a bitfield value that compactly encodes
// information about provided conditional fields.
type Fields uint32
// Has reports whether field with index n was set.
func (f Fields) Has(n int) bool {
return f&(1<<n) != 0
}
// Set sets field with index n.
func (f *Fields) Set(n int) {
*f |= 1 << n
}
uint32.
:
// msg flags:# escape:flags.0?true ttl_seconds:flags.1?int = Message;
type FieldsMessage struct {
Flags bin.Fields
Escape bool
TTLSeconds int
}
func (f *FieldsMessage) Encode(b *bin.Buffer) error {
b.PutID(FieldsMessageTypeID)
if f.Escape {
f.Flags.Set(0)
}
if err := f.Flags.Encode(b); err != nil {
return err
}
if f.Flags.Has(1) {
b.PutInt(f.TTLSeconds)
}
return nil
}
, TTLSeconds
1
, Escape
Flags
.
int128 int256:
int128 4*[ int ] = Int128; int256 8*[ int ] = Int256;
go :
type Int128 [16]byte
type Int256 [32]byte
, :
func (b *Buffer) PutInt128(v Int128) {
b.Buf = append(b.Buf, v[:]...)
}
func (b *Buffer) PutInt256(v Int256) {
b.Buf = append(b.Buf, v[:]...)
}
, big.Int.
MTProto big-endian OpenSSL. Go big.Int
.
var v Int256
i := new(big.Int).SetBytes(v[:]) // v -> i
i.FillBytes(v[:]) // i -> v
bin
, . , , .
Este problema se resuelve generando código de (des) serialización a partir del esquema (¡por eso escribimos un analizador!). Quizás le dé al generador una parte separada en una serie de artículos. Este módulo de proyecto resultó ser complejo, se reescribió varias veces y me gustaría facilitar un poco la vida a las personas que escribirán generadores de código en Go para otros formatos.
Como referencia, actualmente se generan alrededor de 180K SLOC a partir de esquemas de telegramas (api, mtproto, chats secretos).
¡Me gustaría agradecer a tdakkota y zweihander por su invaluable contribución al desarrollo del proyecto! Sería muy difícil sin ti.