私のお気に入りのメッセンジャーのために高品質のクライアントを外出先で書きたいという願望は長い間熟してきましたが、ほんの1か月前に、その時が来たと判断し、これには十分な資格がありました。
開発はまだ進行中です(そして完全にオープンソースです)が、魅力的な道はすでにプロトコルの完全な理解の欠如から比較的安定したクライアントへと移っています。一連の記事では、私が直面した課題とその対処方法について説明します。私が適用した手法は、スキーマを使用して任意のバイナリプロトコル用のクライアントを開発するときに役立ちます。
タイプ言語
タイプ言語またはTL、プロトコル記述スキームから始めましょう。フォーマットの説明については詳しく説明しません。Habréにはすでに分析があります。簡単に説明します。これはgRPCにいくぶん似ており、クライアントとサーバー間の相互作用スキーム(データ構造と一連のメソッド)について説明しています。
タイプの説明の例を次に示します。
error#1fbadfee code:int32 message:string = Error;
ここでは1fbadfee
、これはid型で、error
その名前は、コードとメッセージがフィールドであり、Error
これはクラス名です。
メソッドは同様の方法で記述されますが、タイプ名の代わりにメソッド名があり、クラスの代わりに結果タイプがあります。
sendPM#3faceff text:string habrauser:string = Error;
これは、メソッドsendPM
が引数text
とを受け取りhabrauser
、Error
以前に説明されたバリアント(コンストラクター)を返すことを意味しますerror#1fbadfee
。
, - . : ad-hoc, .. . participle, go, . ad-hoc .
, , , . : , , .
, . Definition
, :
func TestDefinition(t *testing.T) {
for _, tt := range []struct {
Case string
Input string
String string
Definition Definition
}{
{
Case: "inputPhoneCall",
Input: "inputPhoneCall#1e36fded id:long access_hash:long = InputPhoneCall",
Definition: Definition{
ID: 0x1e36fded,
Name: "inputPhoneCall",
Params: []Parameter{
{
Name: "id",
Type: bareLong,
},
{
Name: "access_hash",
Type: bareLong,
},
},
Type: Type{Name: "InputPhoneCall"},
},
},
// ...
} {
t.Run(tt.Case, func(t *testing.T) {
var d Definition
if err := d.Parse(tt.Input); err != nil {
t.Fatal(err)
}
require.Equal(t, tt.Definition, d)
})
}
}
, Flag
( , ), .
, , . :
t.Run("Error", func(t *testing.T) {
for _, invalid := range []string{
"=0",
"0 :{.0?InputFi00=0",
} {
t.Run(invalid, func(t *testing.T) {
var d Definition
if err := d.Parse(invalid); err == nil {
t.Error("should error")
}
})
}
})
testdata
. _testdata
: , , go .
Sample.tl _testdata :
func TestParseSample(t *testing.T) {
data, err := ioutil.ReadFile(filepath.Join("_testdata", "Sample.tl"))
if err != nil {
t.Fatal(err)
}
schema, err := Parse(bytes.NewReader(data))
if err != nil {
t.Fatal(err)
}
// ...
}
go , , filepath.Join
-.
(golden)
"golden files". , . , ( -update
). , . goldie .
func TestParser(t *testing.T) {
for _, v := range []string{
"td_api.tl",
"telegram_api.tl",
"telegram_api_header.tl",
"layer.tl",
} {
t.Run(v, func(t *testing.T) {
data, err := ioutil.ReadFile(filepath.Join("_testdata", v))
if err != nil {
t.Fatal(err)
}
schema, err := Parse(bytes.NewReader(data))
if err != nil {
t.Fatal(err)
}
t.Run("JSON", func(t *testing.T) {
g := goldie.New(t,
goldie.WithFixtureDir(filepath.Join("_golden", "parser", "json")),
goldie.WithDiffEngine(goldie.ColoredDiff),
goldie.WithNameSuffix(".json"),
)
g.AssertJson(t, v, schema)
})
})
}
}
, json ( json). -update
, , _golden
.
(, json ) , .
Decode-Encode-Decode
, , decode-encode-decode, .
String() string
:
// Annotation represents an annotation comment, like //@name value.
type Annotation struct {
Name string `json:"name"`
Value string `json:"value"`
}
func (a Annotation) String() string {
var b strings.Builder
b.WriteString("//")
b.WriteRune('@')
b.WriteString(a.Name)
b.WriteRune(' ')
b.WriteString(a.Value)
return b.String()
}
, strings.Builder, String()
.
, , .
Fuzzing
() . , , (coverage-guided fuzzing). go go-fuzz . ( ) , . , syzkaller, go, Linux .
, , , , .
, Definition:
// +build fuzz
package tl
import "fmt"
func FuzzDefinition(data []byte) int {
var d Definition
if err := d.Parse(string(data)); err != nil {
return 0
}
var other Definition
if err := other.Parse(d.String()); err != nil {
fmt.Printf("input: %s\n", string(data))
fmt.Printf("parsed: %#v\n", d)
panic(err)
}
return 1
}
, .
Decode-encode-decode-encode
We need to go deeper. :
(2)
(3)
(4) (2)
(4) (2) , .. - . , .
go-fuzz
Denial of Service , .. OOM. , go-fuzz , , .
corpus, , ( crashers, , , ). crashers , 0, . , , corpus , .
, , , - . (STUN, TURN, SDP, MTProto, ...) .
, - . , , ( ) Telegram go:
( )
-
ネットワーク通信テスト(ユニット、e2e)
副作用(時間、タイムアウト、PRNG)を伴うテスト作業
CI、またはマージボタンを押すのが怖くないようにパイプラインを設定します
また、プロジェクトに参加したプロジェクト参加者のおかげで、彼らがいなければそれははるかに困難になるでしょう。