Dans la partie précédente , nous avons décrit les approches utilisées lors de l'écriture d'un analyseur pour le schéma MTProto. L'article s'est avéré un peu plus général que ce à quoi je m'attendais, cette fois je vais essayer d'en dire plus sur les spécificités de Telegram.
Le client Go continue d'évoluer , et nous allons remonter le temps et nous rappeler comment le sérialiseur et le désérialiseur de protocole ont été écrits pour lui .
Les bases
Il existe deux façons de désérialiser: en streaming et en mémoire tampon. En pratique, dans MTProto, un message de plus d'un mégaoctet ne peut pas être transmis, j'ai donc choisi l'option avec un tampon: disons que nous pouvons toujours garder un message complet en mémoire.
Vous obtenez la structure suivante:
// Buffer implements low level binary (de-)serialization for TL.
type Buffer struct {
Buf []byte
}
Et pourtant, MTProto aligne fondamentalement les valeurs de 4 octets (32 bits), mettons cela dans une constante:
// Word represents 4-byte sequence.
// Values in TL are generally aligned to Word.
const Word = 4
Sérialisation
Sachant que presque tout dans MTProto est petit-boutiste, nous pouvons commencer par sérialiser 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...)
}
Nous sérialiserons toutes les autres valeurs de la même manière: d'abord, nous avons alloué la tranche (le compilateur Go est assez intelligent pour ne pas la mettre dans le tas dans ce cas, car la taille de la tranche est petite et constante), puis nous avons écrit la valeur là, puis a ajouté la tranche au tampon.
, , . , 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
, . , , .
Ce problème est résolu en générant du code de (dé-) sérialisation à partir du schéma (c'est pourquoi nous avons écrit un analyseur!). Je vais peut-être donner au générateur une partie distincte dans une série d'articles. Ce module de projet s'est avéré compliqué, il a été réécrit plusieurs fois et je voudrais rendre la vie un peu plus facile aux personnes qui écriront des générateurs de code en Go pour d'autres formats.
Pour référence, environ 180K SLOC sont actuellement générés à partir de schémas de télégramme (api, mtproto, chats secrets).
Je tiens à remercier tdakkota et zweihander pour leur précieuse contribution au développement du projet! Ce serait très difficile sans vous.