compress
стандартной библотеки;gzip
, который широко используется в Linux. У него следующая структура: gzip
по умолчанию предназначен для сжатия одного файла, поэтому коллекцию файлов предварительно объединяют в один архив tar
и сжимают в формате gzip
. У сжатого файла будет расширение .tar.gz
.bzip2
;gzip
(использует алгоритм Deflate);zlib
(использует алгоритм Deflate).zip
-архивах используются алгоритмы bzip2, LZW и Deflate, а формат gzip
применяется в gz
архивах.compress/flate
и compress/gzip
. Первый пакет реализует часто используемый алгоритм сжатия Deflate, а второй обеспечивает поддержку формата gzip
, используемого при сжатии HTML-страниц. Работа с остальными пакетами аналогична.compress
принимают в качестве параметров переменные, которые реализуют интерфейсы io.Reader и io.Writer. В каждом пакете обычно есть два типа: Writer
для сжатия и Reader
для распаковки данных.*gzip.Reader
в функцию gzip.NewReader(r io.Reader)
передаётся io.Reader
со сжатыми данными. При этом тип *gzip.Reader
тоже реализует интерфейс io.Reader
, из него можно читать уже распакованные данные.gzip.NewWriter(w io.Writer)
принимает io.Writer
, в который будут сохраняться сжатые данные. Сам *gzip.Writer
реализует интерфейс io.Writer
, в который пишутся исходные данные.// Compress сжимает слайс байт.
func Compress(data []byte) ([]byte, error) {
var b bytes.Buffer
// создаём переменную w — в неё будут записываться входящие данные,
// которые будут сжиматься и сохраняться в bytes.Buffer
w, err := flate.NewWriter(&b, flate.BestCompression)
if err != nil {
return nil, fmt.Errorf("failed init compress writer: %v", err)
}
// запись данных
_, err = w.Write(data)
if err != nil {
return nil, fmt.Errorf("failed write data to compress temporary buffer: %v", err)
}
// обязательно нужно вызвать метод Close() — в противном случае часть данных
// может не записаться в буфер b; если нужно выгрузить все упакованные данные
// в какой-то момент сжатия, используйте метод Flush()
err = w.Close()
if err != nil {
return nil, fmt.Errorf("failed compress data: %v", err)
}
// переменная b содержит сжатые данные
return b.Bytes(), nil
}
// Decompress распаковывает слайс байт.
func Decompress(data []byte) ([]byte, error) {
// переменная r будет читать входящие данные и распаковывать их
r := flate.NewReader(bytes.NewReader(data))
defer r.Close()
var b bytes.Buffer
// в переменную b записываются распакованные данные
_, err := b.ReadFrom(r)
if err != nil {
return nil, fmt.Errorf("failed decompress data: %v", err)
}
return b.Bytes(), nil
}
flate.NewWriter()
принимает переменную интерфейсного типа io.Writer
. В нашем примере это bytes.Buffer
. Если указать переменную типа os.File
, сжатые данные будут сразу записываться в файл.flate.NewReader()
. Для неё источником сжатых данных может быть переменная типа os.File
или другого типа с поддержкой интерфейса io.Reader
.strings.Repeat()
создаст большое количество повторяющихся подстрок. Вызовем созданную функцию Compress()
.func main() {
data := []byte(strings.Repeat(`This is a test message`, 20))
// сжимаем содержимое data
b, err := Compress(data)
if err != nil {
log.Fatal(err)
}
fmt.Printf("%d bytes has been compressed to %d bytes\r\n", len(data), len(b))
// распаковываем сжатые данные
out, err := Decompress(b)
if err != nil {
log.Fatal(err)
}
// сравниваем начальные и полученные данные
if !bytes.Equal(data, out) {
log.Fatal(`original data != decompressed data`)
}
}
440 bytes has been compressed to 33 bytes
. Получилось сжать строчку в 12 раз!Алгоритм | Степень сжатия,% | Скорость упаковки, MiB/sec | Скорость распаковки, MiB/sec |
---|---|---|---|
Zstandard | 18.54 | 0.64 | 322.88 |
Brotli | 19.49 | 0.34 | 201.15 |
gzip | 26.31 | 7.72 | 122.91 |
LZ4 | 30.24 | 12.06 | 1258.51 |
Snappy | 41.29 | 132.59 | 882.18 |
io.Writer
и io.Reader
. Благодаря этому при разработке можно переключаться с одного алгоритма на другой без больших изменений в коде и выбирать оптимальный вариант для конкретной задачи.func BrotliCompress(data []byte) ([]byte, error) {
var buf bytes.Buffer
w := brotli.NewWriterLevel(&buf, brotli.BestCompression)
_, err := w.Write(data)
if err != nil {
return nil, err
}
err = w.Close()
if err != nil {
return nil, err
}
return buf.Bytes(), err
}
Compress
, которая была рассмотрена выше. Фактически, замена функции flate.NewWriter()
на brotli.NewWriterLevel()
позволяет переключится с одного алгоритма сжатия на другой.Flush()
. Зачем он нужен?io.Writer
. Отмечу, что при вызове Close()
нет нужды вызыватьFlush()
.net/http
. Напомним основные принципы. http.ResponseWriter
и на лету сможет упаковывать данные в нужный формат.Accept-Encoding
указывает на то, какой формат сжатых данных поддерживается клиентом:gzip
— сжатие gzip;compress
— сжатие LZW;deflate
— сжатие zlib;br
— сжатие brotli.;q=
для конкретных форматов.Accept-Encoding: gzip
Accept-Encoding: gzip, deflate, br
Accept-Encoding: deflate, gzip;q=1.0
Accept-Encoding
и обнаружил поддержку нужного формата сжатия, он может сжать тело ответа и отправить клиенту. Так как сжатие данных — необязательная операция, сервер должен в ответе указать признак, по которому клиент поймёт, что ответ сервера нужно распаковать.Content-Encoding
должно равняться одному из значений Accept-Encoding
и указывать на используемый формат сжатия.Content-Encoding: gzip
gzip
-сжатия. Пусть сервер возвращает страницу с 20 строками Hello, world
.func defaultHandle(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/html")
io.WriteString(w, "<html><body>"+strings.Repeat("Hello, world<br>", 20)+"</body></html>")
}
gzip
должен подменить http.ResponseWriter
и сразу же упаковать данные в нужный формат.gzipWriter
, поддерживающий интерфейс http.ResponseWriter
.type gzipWriter struct {
http.ResponseWriter
Writer io.Writer
}
func (w gzipWriter) Write(b []byte) (int, error) {
// w.Writer будет отвечать за gzip-сжатие, поэтому пишем в него
return w.Writer.Write(b)
}
http.ResponseWriter
указан без имени поля, он встраивается в тип gzipWriter
, который содержит все методы этого интерфейса. В противном случае нужно было бы описать методы Header
и WriteHeader
. В примере для gzipWriter
достаточно переопределить метод Write
.http.Handler
и возвращает себя как новый обработчик. Таким образом, запрос к серверу попадёт в эту функцию, где при необходимости можно заменить обычный Writer
на gzip.Writer
, и он обеспечит сжатие данных. Если вы используете для управления HTTP-сервером внешний пакет, то подключение middleware может отличаться от приведённого примера.func gzipHandle(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// проверяем, что клиент поддерживает gzip-сжатие
// это упрощённый пример. В реальном приложении следует проверять все
// значения r.Header.Values("Accept-Encoding") и разбирать строку
// на составные части, чтобы избежать неожиданных результатов
if !strings.Contains(r.Header.Get("Accept-Encoding"), "gzip") {
// если gzip не поддерживается, передаём управление
// дальше без изменений
next.ServeHTTP(w, r)
return
}
// создаём gzip.Writer поверх текущего w
gz, err := gzip.NewWriterLevel(w, gzip.BestSpeed)
if err != nil {
io.WriteString(w, err.Error())
return
}
defer gz.Close()
w.Header().Set("Content-Encoding", "gzip")
// передаём обработчику страницы переменную типа gzipWriter для вывода данных
next.ServeHTTP(gzipWriter{ResponseWriter: w, Writer: gz}, r)
})
}
func main() {
mux := http.NewServeMux()
mux.HandleFunc("/", defaultHandle)
http.ListenAndServe(":3000", gzipHandle(mux))
}
gzip
. Если добавить в сервер другие страницы, они тоже будут автоматически сжиматься. Запустите программу и посмотрите в браузере, как работает сжатие.Content-Type
, при которых будет происходить сжатие. Например:application/javascript
application/json
text/css
text/html
text/plain
text/xml
Content-Type
на соответствие данному списку.gzip.NewWriterLevel
и использовать метод gzip.Reset
, чтобы избежать выделения памяти при каждом запросе.gzip
, и изучить их реализацию. Accept-Encoding: gzip
и распаковывает полученный ответ. В этом случае не нужно добавлять распаковку в код клиента. gzip
, распаковывает и возвращает их оригинальный размер.// LengthHandle возвращает размер распакованных данных.
func LengthHandle(w http.ResponseWriter, r *http.Request) {
// создаём *gzip.Reader, который будет читать тело запроса
// и распаковывать его
gz, err := gzip.NewReader(r.Body)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
// закрытие gzip-читателя опционально, т.к. все данные уже прочитаны и
// текущая реализация не требует закрытия, тем не менее лучше это делать -
// некоторые реализации могут рассчитывать на закрытие читателя
// gz.Close() не вызывает закрытия r.Body - это будет сделано позже, http-сервером
defer gz.Close()
// при чтении вернётся распакованный слайс байт
body, err := io.ReadAll(gz)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
fmt.Fprintf(w, "Length: %d", len(body))
}
gzip
-данных не сильно отличается от распаковки Deflate. Этот обработчик считает, что к нему приходят только упакованные данные. Если в запросе отправить обычный текст, вернётся ошибка.middleware
, чтобы отделить реализацию хендлера запросов от реализации сжатия и декомпрессии. Тогда хендлер не будет заботиться о том, сжимались ли данные на стороне клиента. Он просто продолжит пользоваться стандартными объектами http.ResponseWriter
и *http.Request
, как и раньше.cmd/skill/gzip.go
:> ~/dev/alice-skill
|
|--- cmd
| |--- skill
| |--- flags.go
| |--- gzip.go
| |--- main.go
| |--- main_test.go
|--- internal
| |--- logger
| | |--- logger.go
| |--- models
| |--- models.go
|--- go.mod
|--- go.sum
http.ResponseWriter
, и поле Body
объекта *http.Request
:package main
import (
"compress/gzip"
"io"
"net/http"
)
// compressWriter реализует интерфейс http.ResponseWriter и позволяет прозрачно для сервера
// сжимать передаваемые данные и выставлять правильные HTTP-заголовки
type compressWriter struct {
w http.ResponseWriter
zw *gzip.Writer
}
func newCompressWriter(w http.ResponseWriter) *compressWriter {
return &compressWriter{
w: w,
zw: gzip.NewWriter(w),
}
}
func (c *compressWriter) Header() http.Header {
return c.w.Header()
}
func (c *compressWriter) Write(p []byte) (int, error) {
return c.zw.Write(p)
}
func (c *compressWriter) WriteHeader(statusCode int) {
if statusCode < 300 {
c.w.Header().Set("Content-Encoding", "gzip")
}
c.w.WriteHeader(statusCode)
}
// Close закрывает gzip.Writer и досылает все данные из буфера.
func (c *compressWriter) Close() error {
return c.zw.Close()
}
// compressReader реализует интерфейс io.ReadCloser и позволяет прозрачно для сервера
// декомпрессировать получаемые от клиента данные
type compressReader struct {
r io.ReadCloser
zr *gzip.Reader
}
func newCompressReader(r io.ReadCloser) (*compressReader, error) {
zr, err := gzip.NewReader(r)
if err != nil {
return nil, err
}
return &compressReader{
r: r,
zr: zr,
}, nil
}
func (c compressReader) Read(p []byte) (n int, err error) {
return c.zr.Read(p)
}
func (c *compressReader) Close() error {
if err := c.r.Close(); err != nil {
return err
}
return c.zr.Close()
}
main.go
. Добавим в него middleware
-функцию и обернём ею хендлер:func gzipMiddleware(h http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
// по умолчанию устанавливаем оригинальный http.ResponseWriter как тот,
// который будем передавать следующей функции
ow := w
// проверяем, что клиент умеет получать от сервера сжатые данные в формате gzip
acceptEncoding := r.Header.Get("Accept-Encoding")
supportsGzip := strings.Contains(acceptEncoding, "gzip")
if supportsGzip {
// оборачиваем оригинальный http.ResponseWriter новым с поддержкой сжатия
cw := newCompressWriter(w)
// меняем оригинальный http.ResponseWriter на новый
ow = cw
// не забываем отправить клиенту все сжатые данные после завершения middleware
defer cw.Close()
}
// проверяем, что клиент отправил серверу сжатые данные в формате gzip
contentEncoding := r.Header.Get("Content-Encoding")
sendsGzip := strings.Contains(contentEncoding, "gzip")
if sendsGzip {
// оборачиваем тело запроса в io.Reader с поддержкой декомпрессии
cr, err := newCompressReader(r.Body)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
return
}
// меняем тело запроса на новое
r.Body = cr
defer cr.Close()
}
// передаём управление хендлеру
h.ServeHTTP(ow, r)
}
}
// ...
func run() error {
if err := logger.Initialize(flagLogLevel); err != nil {
return err
}
logger.Log.Info("Running server", zap.String("address", flagRunAddr))
// оборачиваем хендлер webhook в middleware с логированием и поддержкой gzip
return http.ListenAndServe(flagRunAddr, logger.RequestLogger(gzipMiddleware(webhook)))
}
main_test.go
следующий код:// ...
func TestGzipCompression(t *testing.T) {
handler := http.HandlerFunc(gzipMiddleware(webhook))
srv := httptest.NewServer(handler)
defer srv.Close()
requestBody := `{
"request": {
"type": "SimpleUtterance",
"command": "sudo do something"
},
"version": "1.0"
}`
// ожидаемое содержимое тела ответа при успешном запросе
successBody := `{
"response": {
"text": "Извините, я пока ничего не умею"
},
"version": "1.0"
}`
t.Run("sends_gzip", func(t *testing.T) {
buf := bytes.NewBuffer(nil)
zb := gzip.NewWriter(buf)
_, err := zb.Write([]byte(requestBody))
require.NoError(t, err)
err = zb.Close()
require.NoError(t, err)
r := httptest.NewRequest("POST", srv.URL, buf)
r.RequestURI = ""
r.Header.Set("Content-Encoding", "gzip")
resp, err := http.DefaultClient.Do(r)
require.NoError(t, err)
require.Equal(t, http.StatusOK, resp.StatusCode)
defer resp.Body.Close()
b, err := io.ReadAll(resp.Body)
require.NoError(t, err)
require.JSONEq(t, successBody, string(b))
})
t.Run("accepts_gzip", func(t *testing.T) {
buf := bytes.NewBufferString(requestBody)
r := httptest.NewRequest("POST", srv.URL, buf)
r.RequestURI = ""
r.Header.Set("Accept-Encoding", "gzip")
resp, err := http.DefaultClient.Do(r)
require.NoError(t, err)
require.Equal(t, http.StatusOK, resp.StatusCode)
defer resp.Body.Close()
zr, err := gzip.NewReader(resp.Body)
require.NoError(t, err)
b, err := io.ReadAll(zr)
require.NoError(t, err)
require.JSONEq(t, successBody, string(b))
})
}
=== RUN Test_webhook
--- PASS: Test_webhook (0.00s)
...
=== RUN TestGzipCompression
--- PASS: TestGzipCompression (0.00s)
=== RUN TestGzipCompression/sends_gzip
--- PASS: TestGzipCompression/sends_gzip (0.00s)
=== RUN TestGzipCompression/accepts_gzip
--- PASS: TestGzipCompression/accepts_gzip (0.00s)
PASS
Process finished with the exit code 0
archive/tar
и archive/zip
предназначены для создания и распаковки архивных файлов соответствующих форматов. Если нужна поддержка других архивов, можно воспользоваться пакетом mholt/archiver.gziphandler
.mholt/archiver
для работы с архивами.