Im vorherigen Teil haben wir die Ansätze beschrieben, die beim Schreiben eines Parsers für das MTProto-Schema verwendet werden. Der Artikel erwies sich als etwas allgemeiner als ich erwartet hatte. Dieses Mal werde ich versuchen, mehr über die Besonderheiten von Telegram zu erzählen.
Der Go-Client entwickelt sich weiter und wir werden in die Vergangenheit reisen und uns daran erinnern, wie der Protokoll- Serializer und -Deserializer dafür geschrieben wurde .
Die Grundlagen
Es gibt zwei Möglichkeiten zum Deserialisieren: gestreamt und gepuffert. In der Praxis kann in MTProto keine Nachricht übertragen werden, die größer als ein Megabyte ist. Daher habe ich die Option mit einem Puffer ausgewählt: Nehmen wir an, wir können immer eine vollständige Nachricht im Speicher behalten.
Sie erhalten folgende Struktur:
// Buffer implements low level binary (de-)serialization for TL.
type Buffer struct {
Buf []byte
}
Und dennoch richtet MTProto die Werte grundsätzlich um 4 Bytes (32 Bit) aus. Lassen Sie uns dies in eine Konstante setzen:
// Word represents 4-byte sequence.
// Values in TL are generally aligned to Word.
const Word = 4
Serialisierung
Da wir wissen, dass fast alles in MTProto Little-Endian ist, können wir zunächst uint32 serialisieren:
// 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...)
}
Wir werden alle anderen Werte auf die gleiche Weise serialisieren: Zuerst haben wir das Slice zugewiesen (der Go-Compiler ist klug genug, um es in diesem Fall nicht in den Heap zu legen, da die Slice-Größe klein und konstant ist), dann haben wir geschrieben den Wert dort und fügte dann das Slice zum Puffer hinzu.
, , . , 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
, . , , .
Dieses Problem wird gelöst, indem aus dem Schema (De-) Serialisierungscode generiert wird (deshalb haben wir einen Parser geschrieben!). Vielleicht gebe ich dem Generator einen separaten Teil in einer Reihe von Artikeln. Dieses Projektmodul stellte sich als kompliziert heraus, es wurde mehrmals umgeschrieben und ich möchte Menschen, die Codegeneratoren in Go für andere Formate schreiben, das Leben ein wenig erleichtern.
Als Referenz werden derzeit etwa 180 KB SLOC aus Telegrammschemata (API, MTPROO, geheime Chats) generiert.
Ich möchte tdakkota und zweihander für ihren unschätzbaren Beitrag zur Entwicklung des Projekts danken ! Ohne dich wäre es sehr schwierig.