...

пятница, 20 февраля 2015 г.

Пишем симулятор медленных соединений на Go

В этой статье я хочу показать, как просто в Go можно делать достаточно сложные вещи, и какую мощь в себе несут интерфейсы. Речь пойдет о симуляции медленного соединения — но, в отличие от популярных решений в виде правил для iptables, мы реализуем это на стороне кода — так, чтобы можно было легко использовать, к примеру, в тестах.

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






Интерфейсы




Интерфейсы — это специальный тип в системе типов Go, позволяющий описывать поведение объекта. Любой статический тип, для которого определены методы (поведение) неявно реализует интерфейс, который описывает эти методы. Самый известный пример — интерфейс из стандартной библиотеки io.Reader:

// Reader is the interface that wraps the basic Read method.
// ...
type Reader interface {
Read(p []byte) (n int, 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(r)

type Sample 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
}

return SlowConn{conn, slow.NewReader(conn, 100)}, nil
},
},
}


Ну а теперь склеиваем это всё вместе и смотрим результат:


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


Заключение




Как говорилось в начале статьи, интерфейсы — чрезвычайно мощный инструментарий, да и сама идея разделения типов для свойств и для поведения — очень правильная. Но по-настоящему эта мощь проявляется, когда интерфейсы действительно используются по назначению в разных библиотеках. Это позволяет соединять очень разный функционал, и использовать чужой код для вещей, о которых оригинальный автор мог даже не подозревать. И речь не только о стандартных интерфейсах — внутри больших проектов интерфейсы дают огромную гибкость и модульность.

Ссылки




Поскольку идея этого поста была нагло утянута из твиттера Francesc Campoy, то только одна ссылка :)

http://ift.tt/1z4iMUY

This entry passed through the Full-Text RSS service - if this is your content and you're reading it on someone else's site, please read the FAQ at http://ift.tt/jcXqJW.


Комментариев нет:

Отправить комментарий