Обработчик HTTP в Golang с Gzip

Введение

В стандартной библиотеке Golang есть много видов техники сжатия, которые вы можете использовать для сжатия данных. Сжатие необходимо для уменьшения размера данных. Даже в веб-сервере техника сжатия будет полезна для увеличения скорости обмена данными между клиентом и сервером. Gzip — одна из техник сжатия, поддерживаемая как Golang, так и web. В этой статье мы рассмотрим создание HTTP-обработчика golang, который (де)сжимает gzip-запросы/ответы, и как создать HTTP-запрос, который отправляет/получает сжатое gzip-тело полезной нагрузки. Весь код, рассмотренный в этой статье, вы можете найти в этом репозитории.

Структура каталога

$ tree .
.
├── LICENSE
├── Makefile
├── README.md
├── client
│   └── main.go
├── curl.sh
├── go.mod
├── server
│   └── main.go
└── util.go

2 directories, 8 files
Вход в полноэкранный режим Выход из полноэкранного режима

server/main.go

package main

import (
    "example"
    "log"
    "net/http"
)

func main() {
    log.Println("server listening at :8000")
    http.ListenAndServe(":8000", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        body := example.MustReadCompressedBody[example.Payload](r.Body)
        body.Number++

        example.MustWriteCompressedResponse(w, body)
    }))
}
Вход в полноэкранный режим Выход из полноэкранного режима

Сервер запускает только одну конечную точку, которая будет читать сжатое тело, увеличивать содержимое полезной нагрузки, затем отвечать новым телом.

util.go

Полезная нагрузка

Вы можете использовать простейшую полезную нагрузку просто для проверки сжатия, например:

type Payload struct {
    Number int `json:"number"`
}
Войти в полноэкранный режим Выйти из полноэкранного режима

Чтение сжатого тела полезной нагрузки

func MustReadCompressedBody[T any](r io.Reader) *T {
    gr, err := gzip.NewReader(r)
    PanicIfErr(err)
    defer gr.Close()

    var t T
    PanicIfErr(json.NewDecoder(gr).Decode(&t))
    return &t
}
Войти в полноэкранный режим Выйти из полноэкранного режима

Чтобы прочитать полезную нагрузку, сжатую gzip, вам нужно создать gzip.Reader из ответа или запроса. Затем декодируйте JSON, как обычно, используя экземпляр gzip.Reader.

Запись сжатого ответа

func MustWriteCompressedResponse(w http.ResponseWriter, body any) {
    w.Header().Set("Content-Type", "application/json")
    w.Header().Set("Content-Encoding", "gzip")

    gw := gzip.NewWriter(w)
    defer gw.Close()
    PanicIfErr(json.NewEncoder(gw).Encode(body))
}
Вход в полноэкранный режим Выйти из полноэкранного режима

Как обычно, лучше сообщить клиенту о вашем Content-Type, и Content-Encoding. Но в случае с golang, информирование клиента о Content-Encoding поможет golang http.Request автоматически распаковать полезную нагрузку. Как и при чтении полезной нагрузки, вам нужно только создать gzip.Writer, а затем нормально закодировать содержимое. Также не забудьте вызвать gw.Close(), чтобы избежать ошибки EOF.

Тестирование сервера

Теперь ваш сервер полностью (де)сжимает запрос и ответ. Давайте сначала попробуем протестировать его с помощью команды cURL.

# run the server on another terminal
# using `go run ./server/` command
$ echo '{"number": 99}' | gzip | 
curl -iXPOST http://localhost:8000/ 
-H "Content-Type: application/json" 
-H "Content-Encoding: gzip" 
--compressed --data-binary @-
Вход в полноэкранный режим Выйти из полноэкранного режима

Поскольку сервер увеличит Payload.Number, вы можете ожидать, что количество ответов будет 100, отправив запрос с 99. Ожидаемый результат будет выглядеть примерно так:

HTTP/1.1 200 OK
Content-Encoding: gzip
Content-Type: application/json
Date: Thu, 26 May 2022 12:04:53 GMT
Content-Length: 39

{"number":100}
Войти в полноэкранный режим Выход из полноэкранного режима

Теперь, после того как вы убедились, что ваш сервер работает совершенно нормально, давайте попробуем отправить HTTP-запрос, используя golang http.Request.

client/main.go

package main

import (
    "context"
    "encoding/json"
    "example"
    "log"
    "net/http"
    "time"
)

func main() {
    ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
    defer cancel()

    body := &example.Payload{
        Number: 100,
    }

    log.Printf("create compressed request with %#v", body)
    req := example.MustCreateCompressedRequest(ctx, http.MethodPost, "http://localhost:8000/", body)
    defer req.Body.Close()

    log.Printf("send compressed request")
    resp, err := http.DefaultClient.Do(req)
    example.PanicIfErr(err)
    defer resp.Body.Close()

    log.Println("resp.Uncompressed?", resp.Uncompressed)
    var responsePayload *example.Payload
    if resp.Uncompressed {
        err = json.NewDecoder(resp.Body).Decode(&responsePayload)
    } else {
        responsePayload = example.MustReadCompressedBody[example.Payload](resp.Body)
    }
    log.Printf("read response %#v", responsePayload)
}
Вход в полноэкранный режим Выйти из полноэкранного режима

Как и в обычном HTTP-запросе, вам просто нужно создать HTTP.Request, отправить запрос и, наконец, декодировать ответ. Но прежде чем отправлять полезную нагрузку запроса, не забудьте сначала сжать ее.

Создание сжатого запроса

func MustCreateCompressedRequest(ctx context.Context, method, url string, body any) *http.Request {
    pr, pw := io.Pipe()

    go func() {
        gw := gzip.NewWriter(pw)
        err := json.NewEncoder(gw).Encode(body)
        defer PanicIfErr(gw.Close())
        defer pw.CloseWithError(err)
    }()

    r, err := http.NewRequestWithContext(ctx, method, url, pr)
    PanicIfErr(err)

    r.Header.Set("Content-Type", "application/json")
    r.Header.Set("Content-Encoding", "gzip")

    return r
}
Войдите в полноэкранный режим Выйти из полноэкранного режима

Создать новый сжатый запрос довольно просто, так же как и записать сжатый ответ, нам нужно создать новый gzip.Writer. Код выше использует io.Pipe, чтобы избежать буферизации тела в память и ненужных выделений. Но вот альтернатива, если вы хотите сначала буферизовать тело:

func MustCreateCompressedRequest(ctx context.Context, method, url string, body any) *http.Request {
    var b bytes.Buffer

    gw := gzip.NewWriter(&b)
    err := json.NewEncoder(gw).Encode(body)
    defer PanicIfErr(gw.Close())

    r, err := http.NewRequestWithContext(ctx, method, url, &b)
    PanicIfErr(err)

    r.Header.Set("Content-Type", "application/json")
    r.Header.Set("Content-Encoding", "gzip")

    return r
}
Войдите в полноэкранный режим Выйти из полноэкранного режима

Опять же, gw.Close() здесь также необходим. Если вы не закроете gzip.Writer, вы увидите ошибку EOF, подобную этой:

2022/05/26 19:23:58 http: panic serving [::1]:56436: unexpected EOF
Вход в полноэкранный режим Выйти из полноэкранного режима

Наконец, после создания сжатого запроса вам остается только отправить его как обычный запрос. Но здесь есть одна сложная часть.

var responsePayload *example.Payload
if resp.Uncompressed {
    err = json.NewDecoder(resp.Body).Decode(&responsePayload)
} else {
    responsePayload = example.MustReadCompressedBody[example.Payload](resp.Body)
}
Войти в полноэкранный режим Выход из полноэкранного режима

После получения ответа, который вы хотите распаковать и декодировать, пожалуйста, обратите внимание на resp.Uncompressed. Если сервер возвращает заголовок Content-Encoding: gzip, как сказано здесь, Golang попытается декомпрессировать полезную нагрузку за вас, поэтому вам не нужно использовать example.MustReadCompressedBody[example.Payload](resp.Body). Но если вы добавите r.Header.Set("Accept-Encoding", "gzip") в ваш запрос, он не будет автоматически распакован.

Тестирование клиента

# run the server on another terminal
# using `go run ./server/` command
$ go run ./client
2022/05/26 19:41:34 create compressed request with &example.Payload{Number:100}
2022/05/26 19:41:34 send compressed request
2022/05/26 19:41:34 resp.Uncompressed? true
2022/05/26 19:41:34 read response &example.Payload{Number:101}
Войдите в полноэкранный режим Выйдите из полноэкранного режима

Заключение

Реализация сжатия gzip в HTTP обработчике и запросах Golang может добавить немного сложности в ваш код, но я считаю, что современные браузеры / API шлюзы в наши дни могут легко реализовать это без изменения вашего кода. Также это промежуточное программное обеспечение, созданное NY Times, может помочь вам минимизировать усилия по внедрению этого сжатия. Вы можете найти весь код, использованный в этой статье, здесь.

Спасибо за прочтение!

Оцените статью
Procodings.ru
Добавить комментарий