Na parte anterior , descrevemos as abordagens usadas ao escrever um analisador para o esquema MTProto. O artigo acabou sendo um pouco mais geral do que eu esperava, desta vez tentarei contar mais sobre as especificidades do Telegram.
O cliente Go continua a evoluir , e voltaremos no tempo e lembraremos como o serializador e desserializador de protocolo foi escrito para ele .
O básico
Existem duas maneiras de desserializar: transmitido e armazenado em buffer. Na prática, no MTProto, uma mensagem maior que um megabyte não pode ser transmitida, então escolhi a opção com um buffer: digamos que sempre podemos manter uma mensagem completa na memória.
Você obtém a seguinte estrutura:
// Buffer implements low level binary (de-)serialization for TL.
type Buffer struct {
Buf []byte
}
E ainda, MTProto basicamente alinha os valores por 4 bytes (32 bits), vamos colocar isso em uma constante:
// Word represents 4-byte sequence.
// Values in TL are generally aligned to Word.
const Word = 4
Serialização
Sabendo que quase tudo em MTProto é little-endian, podemos começar 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...)
}
Seriaremos todos os outros valores da mesma maneira: primeiro, alocamos a fatia (o compilador Go é inteligente o suficiente para não colocá-la no heap neste caso, já que o tamanho da fatia é pequeno e constante), então escrevemos o valor lá e, em seguida, adicionou a fatia ao buffer.
, , . , 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 é resolvido gerando código de (des) serialização do esquema (é por isso que escrevemos um analisador!). Talvez eu dê ao gerador uma parte separada em uma série de artigos. Este módulo do projeto acabou sendo complicado, foi reescrito várias vezes e gostaria de tornar a vida um pouco mais fácil para quem vai escrever geradores de código em Go para outros formatos.
Para referência, cerca de 180K SLOC é gerado atualmente a partir de esquemas de telegramas (api, mtproto, chats secretos).
Gostaria de agradecer a tdakkota e zweihander por sua contribuição inestimável para o desenvolvimento do projeto! Seria muito difícil sem você.