Пишемо симулятор повільних з'єднань на Go

У цій статті я хочу показати, як просто в Go можна робити досить складні речі, і яку потужність в собі несуть інтерфейси. Мова піде про симуляції повільного з'єднання — але, на відміну від популярних рішень у вигляді правил для iptables, ми реалізуємо це на стороні коду — так, щоб можна було легко використовувати, наприклад, в тестах.

Нічого складного тут не буде, і заради більшої наочності я записав ascii-анімації за допомогою сервісу asciinema), але, сподіваюся, буде пізнавально.



Інтерфейси

Інтерфейси — це спеціальний тип у системі типів Go, що дозволяє описувати поведінку об'єкта. Будь статичний тип, для якого визначено методи (поведінка) неявно реалізує інтерфейс, який описує ці методи. Найвідоміший приклад — інтерфейс із стандартної бібліотеки io.Reader:
// Reader is the interface that wraps the basic Read method.
// ...
type Reader interface {
Read(p []byte) (int n, err error)
}

Будь-яка структура, для якої ви визначите метод Read([]byte) (int, error) — може використовуватися як io.Reader.

Проста ідея, не удавана спочатку дуже цінною і потужною, приймає зовсім інший вигляд, коли інтерфейси використовуються іншими бібліотеками. Для демонстрації цього стандартна бібліотека і io.Reader — ідеальні кандидати.

Висновок консоль

Отже, почнемо з найпростішого застосування Reader-а — виведемо сходинку в stdout. Звичайно, для цієї мети краще використовувати функції з пакету fmt, але ми ж хочемо продемонструвати роботу Reader-а. Тому створимо змінну типу strings.Reader (яка реалізує io.Reader) і, з допомогою функції io.Copy() — яка, як раз теж працює з io.Reader, скопіюємо це в os.Stdout (яка, в свою чергу, імплементує io.Writer).
package main

import (
"io"
"os"
"strings"
)

func main() {
r := strings.NewReader("Not very long line...")
io.Copy(os.Stdout, r)
}


А тепер, використовуючи композицію (composition), створимо свій тип SlowReader, який буде читати з оригінального Reader-а по одному символу з затримкою, скажімо, в 100 мілісекунд — таким чином, забезпечуючи швидкість 10 байт в секунду.
// SlowReader reads 10 chars per second 
type SlowReader struct { 
r io.Reader 
} 

func (sr SlowReader) Read(p []byte) (int, error) { 
time.Sleep(100 * time.Millisecond) 
return sr.r.Read(p[:1]) 
} 

Що таке p[:1], сподіваюся, пояснювати не треба — просто новий slice, що складається з 1 першого символу від оргинального slice-а.

Все що нам залишається — це використовувати наш strings.Reader в якості оригінального io.Reader-а, та передати у io.Copy() повільний SlowReader! Подивіться, як просто і круто одночасно.
(ascii-каст відкривається у новому вікні, js-скрипти на хабре заборонено вбудовувати)


Ви вже повинні почати підозрювати, що цей простий SlowReader можна використовувати не тільки для виводу на екран. Також можна додати параметр начебто delay. А ще краще — винести SlowReader в окремий package, щоб було легко використовувати в подальших прикладах. Трохи причешем код.

Причісуємо код

Створимо директорію test/habr/slow і перенесемо код туди:
package slow

import (
"io"
"time"
)

type SlowReader struct {
delay time.Duration
r io.Reader
}

func (sr SlowReader) Read(p []byte) (int, error) {
time.Sleep(sr.delay)
return sr.r.Read(p[:1])
}

func NewReader(r io.Reader, bps int) io.Reader {
delay := time.Second / time.Duration(bps)
return SlowReader{
r: r,
delay: delay,
}
}

Або, кому цікаво дивитися ascii-касти, ось так — виносимо в окремий package:


І додаємо параметр delay типу time.Duration:


(Правильніше було б, після виносу коду в окремий пакет, назвати тип Reader — щоб було slow.Reader, а не slow.SlowReader, але скрінкасти вже записаний так).

Читання з файлу

А тепер, практично без зусиль, перевіримо наш SlowReader для повільного читання з файлів. Отримавши змінну типу *os.File, яка зберігає в собі дескриптор відкритого файлу, але при цьому реалізує інтерфейс io.Reader — ми можемо працювати з файлом точно також, як і раніше зі strings.Reader.
package main

import (
"io"
"os"
"test/habr/slow"
)

func main() {
file, err := os.Open("test.txt")
if err != nil {
panic(err)
}
defer file.Close() // close file on exit

r := slow.NewReader(file, 5) // 5 bps

io.Copy(os.Stdout, r)
}

Або так:


Декодируем JSON

Але з читанням з файлу — це занадто просто. Давайте розглянемо приклад трохи цікавіше — JSON-декодер із стандартної бібліотеки. Хоча для зручності пакет encoding/json надає функцію json.Unmarshal(), він також дозволяє працювати з io.Reader за допомогою json.Decoder — з ним можна десериализовать потокові дані в json-форматі.

Ми візьмемо просту json-encoded рядок і будемо її «повільно читати» за допомогою нашого SlowReader-а — а json.Decoder видасть готовий об'єкт тільки після того, як дійдуть всі байти. Щоб це було очевидно, ми додамо в функцію slow.SlowReader.Read() висновок на екран кожного прочитаного символу:
package main

import (
"encoding/json"
"fmt"
"strings"
"test/habr/slow"
)

func main() {
sr := strings.NewReader(`{"value": "some text", "id": 42}`) // encoded json

r := slow.NewReader(sr, 5)
dec := json.NewDecoder®

Sample type struct {
Value (string `json:"value"`
ID int64 `json:"id"`
}

var sample Sample
err := dec.Decode(&sample)
if err != nil {
panic(err)
}

fmt.Println("Decoded JSON value:", sample)
}

Це ж у ascii-касти:


Якщо на вас ще не звалилося усвідомлення можливостей, які дає нам така проста концепція інтерфейсів, то йдемо далі — власне, приходимо до теми поста — використовуємо наш SlowReader для того, щоб повільно завантажувати сторінку з інтернету.

«Повільний» HTTP-клієнт

Вас не повинно дивувати, що io.Reader використовується в стандартній бібліотеці повсевместно — для всього, що вміє щось звідки-небудь читати. Читання з мережі не виняток — io.Reader використовується на декількох рівнях, і захований під капотом такого, начебто, простого однорядкового виклику http.Get(url string).

Для початку напишемо стандартний код для HTTP GET запиту і виведемо відповідь на консоль:
package main

import (
"io"
"net/http"
"os"
)

func main() {
resp, err := http.Get("http://golang.org")
if err != nil {
panic(err)
}
defer resp.Body.Close()

io.Copy(os.Stdout, resp.Body)
}


Для тих, хто ще не встиг познайомитися з net/http бібліотекою — кілька пояснень. http.Get() — це обгортка для методу Get() реалізованого для типу http.Client — але в цій обгортці використовується «підходить для більшості випадків» вже иницилизированная мінлива під назвою DefaultClient. Власне, Client далі виконує всю курну роботу, в тому числі і читає з мережі за допомогою об'єкта типу Transport, який в свою чергу використовує низькорівневий об'єкт типу net.Conn. Спочатку це може здатися заплутаним, але, насправді, це досить легко вивчається простим читанням исходников бібліотеки — ось що-що, а стандартна бібліотека в Go, на відміну від більшості інших мов — це зразковий код, на якому можна (і потрібно) вчитися Go і брати з нього приклад.

Трохи раніше я згадав про «io.Reader використовується на декількох рівнях» і це дійсно так — наприклад resp.Body — це теж io.Reader, але нам він не цікавий, тому що нам цікаво симулювати не тормознутый браузер, а повільне з'єднання — значить потрібно знайти io.Reader, який читає з мережі. І це, забігаючи вперед, змінна типу net.Conn — а значить саме її нам і потрібно перевизначити для нашого кастомного http-клієнта. Ми це можемо зробити за допомогою вбудовування (embedding):
type SlowConn struct {
net.Conn // embedding
r slow.SlowReader // in ascii-cast I use io.Reader here, but this one a bit better
}

// SlowConn is also io.Reader!
func (sc SlowConn) Read(p []byte) (int, error) {
return sc.r.Read(p)
}


Найскладніше тут полягає в тому, щоб все-таки трохи глибше розібратися в пакетах net і net/http із стандартної бібліотеки, і правильно створити наш http.Client, використовує повільний io.Reader. Але, в результаті, нічого складного — сподіваюся, на скринкасте видно логіка, по мірі того, як я поглядаю в код стандартної бібліотеки.

У підсумку виходить наступний клієнт (для реального коду це краще винести в окрему функцію і трохи причесати, але для proof-of-concept прикладу зійде):
client := http.Client{
Transport: &http.Transport{
Dial: func(network address string) net.Conn, error) {
conn, err := net.Dial(network address)
if err != nil {
return nil, err
}

slowConn := SlowConn{
Conn: conn,
r: slow.NewReader(conn, 100), // 10 bytes per second
}

return slowConn, nil
},
},
}


Ну а тепер склеюємо це все разом і дивимося результат:


Наприкінці видно, що HTTP-заголовки виводяться в консоль нормально, а текст, власне, сторінки виводиться з подвоєнням кожного символу — це нормально, бо ми виводимо resp.Body з допомогою io.Copy() і при цьому наша, трохи модифікована, реалізація SlowReader.Read() виводить символ теж.

Висновок

Як говорилося на початку статті, інтерфейси — надзвичайно потужний інструментарій, та й сама ідея поділу типів властивостей і для поведінки — дуже правильна. Але по-справжньому ця міць проявляється, коли інтерфейси дійсно використовуються за призначенням у різних бібліотеках. Це дозволяє з'єднувати дуже різний функціонал, і використовувати чужий код для речей, про які оригінальний автор міг навіть не підозрювати. І мова не тільки про стандартні інтерфейси — всередині великих проектів інтерфейси дають величезну гнучкість і модульність.

Посилання

Оскільки ідея цього поста була нахабно утянута з твіттера Francesc Campoy, то тільки одне посилання :)
twitter.com/francesc/status/563310996845244416

Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.