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

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

6f0cefbe3e18464ebe40802a91afe9b0.jpg

ИнтерфейсыИнтерфейсы — это специальный тип в системе типов 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-скрипты на хабре запрещено встраивать)99aeb728e85d447986fbe5e4fee38c73.png

Вы уже должны начать подозревать, что этот простой 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: 762a7408d5824c36ba332f735591ee35.pngИ добавляем параметр delay типа time.Duration: 1f41a8747b534470abbd5cabbb31100e.png

(Правильнее было бы, после выноса кода в отдельный пакет, назвать тип 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) } Или так: de6bd032d478439da1d847dfa3e16277.pngДекодируем 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®

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-касте: 21ad3bb38b194fecb1f2668aed4cea70.pngЕсли на вас ещё не свалилось осознание возможностей, которые даёт нам такая простая концепция интерфейсов, то идём дальше — собственно, приходим к теме поста — используем наш 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 }, }, } Ну, а теперь склеиваем это всё вместе и смотрим результат: 08f740ca49ab414a84df8cbd10eedd53.png

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

Заключение Как говорилось в начале статьи, интерфейсы — чрезвычайно мощный инструментарий, да и сама идея разделения типов для свойств и для поведения — очень правильная. Но по-настоящему эта мощь проявляется, когда интерфейсы действительно используются по назначению в разных библиотеках. Это позволяет соединять очень разный функционал, и использовать чужой код для вещей, о которых оригинальный автор мог даже не подозревать. И речь не только о стандартных интерфейсах — внутри больших проектов интерфейсы дают огромную гибкость и модульность.Ссылки Поскольку идея этого поста была нагло утянута из твиттера Francesc Campoy, то только одна ссылка :)twitter.com/francesc/status/563310996845244416

© Habrahabr.ru